From 4724b9eee55e82781d22ed061354ad6dcae0f4ea Mon Sep 17 00:00:00 2001 From: Joe Isaacs Date: Wed, 4 Nov 2026 10:21:57 +0000 Subject: [PATCH 01/41] Run TPC-DS SF=10 on NVMe in the PR SQL benchmarks (#11194) Signed-off-by: Joe Isaacs --- .../bench_orchestrator/ci_matrix/catalog.py | 6 +++++- bench-orchestrator/tests/test_matrix.py | 9 ++++++--- docs/developer-guide/benchmarking.md | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/bench-orchestrator/bench_orchestrator/ci_matrix/catalog.py b/bench-orchestrator/bench_orchestrator/ci_matrix/catalog.py index da6e77bd08f..9de8a377055 100644 --- a/bench-orchestrator/bench_orchestrator/ci_matrix/catalog.py +++ b/bench-orchestrator/bench_orchestrator/ci_matrix/catalog.py @@ -217,7 +217,11 @@ benchmark=Benchmark.TPCDS, name="TPC-DS SF=10 on NVME", scale_factor=10.0, - runs={"nightly": DEFAULT}, + runs={ + "pr": DEFAULT, + "pr-full": DEFAULT, + "nightly": DEFAULT, + }, ), BenchmarkCase( id="tpcds-s3-10", diff --git a/bench-orchestrator/tests/test_matrix.py b/bench-orchestrator/tests/test_matrix.py index 681482e870c..6c0a703a6c0 100644 --- a/bench-orchestrator/tests/test_matrix.py +++ b/bench-orchestrator/tests/test_matrix.py @@ -36,12 +36,15 @@ COMPACT_IDS = tuple( benchmark_id for benchmark_id in REGULAR_IDS if benchmark_id not in {"polarsignals", "vortex-queries"} ) -PR_FULL_IDS = (*REGULAR_IDS[:-1], "job-nvme", "vortex-queries") +DEVELOP_IDS = (*REGULAR_IDS[:-1], "job-nvme", "vortex-queries") +# TPC-DS SF=10 on NVMe runs in the PR presets and nightly, not on every develop commit. +PR_IDS = (*REGULAR_IDS[:7], "tpcds-nvme-10", *REGULAR_IDS[7:]) +PR_FULL_IDS = (*PR_IDS[:-1], "job-nvme", "vortex-queries") EXPECTED_IDS = { - "develop": PR_FULL_IDS, + "develop": DEVELOP_IDS, "pr": tuple( benchmark_id - for benchmark_id in REGULAR_IDS + for benchmark_id in PR_IDS if benchmark_id not in { "clickbench-sorted-nvme", diff --git a/docs/developer-guide/benchmarking.md b/docs/developer-guide/benchmarking.md index 8fcc9d756e2..6479cbf9f4a 100644 --- a/docs/developer-guide/benchmarking.md +++ b/docs/developer-guide/benchmarking.md @@ -296,8 +296,8 @@ Benchmarks run automatically on all commits to `develop` and can be run on-deman - **GPU compression** -- `action/bench-gpu-compress` runs the allow-listed Vortex decompression cases on a GPU runner. - **SQL** -- `action/bench-sql` runs the `pr` preset, a quick core subset of the SQL benchmarks - that excludes `vortex-compact`, Clickbench Sorted, Appian, JOB, statpopgen, FineWeb on S3, - TPC-H SF=10 on S3, and Vortex queries. + that includes TPC-DS SF=10 on NVMe and excludes `vortex-compact`, Clickbench Sorted, Appian, + JOB, statpopgen, FineWeb on S3, TPC-H SF=10 on S3, and Vortex queries. - **SQL Extended** -- `action/bench-sql-extended` runs the `pr-full` preset: every regular SQL benchmark, including `vortex-compact` and DuckDB-format targets. - **SQL Compact** -- `action/bench-sql-compact` runs the `pr-compact` preset, which benchmarks From 9c3cbbbbe609e1fb61b327bdb491600d7d2cbe1d Mon Sep 17 00:00:00 2001 From: Joe Isaacs Date: Wed, 4 Nov 2026 11:20:49 +0000 Subject: [PATCH 02/41] Store plan children in fixed-position slots (#11178) Signed-off-by: Joe Isaacs --- .../internals/scan-planning.md | 30 ++- vortex-layout/src/plan/physical/children.rs | 131 ----------- vortex-layout/src/plan/physical/display.rs | 9 +- vortex-layout/src/plan/physical/lower.rs | 92 ++++---- vortex-layout/src/plan/physical/mod.rs | 24 +- .../src/plan/physical/operators/pack.rs | 25 ++- vortex-layout/src/plan/physical/optimize.rs | 48 ++-- .../src/plan/physical/optimizer/mod.rs | 10 +- .../src/plan/physical/optimizer/rules.rs | 4 +- .../src/plan/physical/pipeline/scan.rs | 2 +- .../src/plan/physical/pipeline/synthetic.rs | 8 +- .../src/plan/physical/pipeline/tests.rs | 59 ++++- .../src/plan/physical/plans/concat.rs | 74 ++++--- vortex-layout/src/plan/physical/plans/eval.rs | 25 +-- .../src/plan/physical/plans/filter.rs | 22 +- .../src/plan/physical/plans/legacy_stats.rs | 30 ++- .../src/plan/physical/plans/list_pack.rs | 102 ++++----- vortex-layout/src/plan/physical/plans/pack.rs | 103 +++++---- .../src/plan/physical/plans/row_idx.rs | 11 +- .../src/plan/physical/plans/segment_scan.rs | 11 +- .../src/plan/physical/plans/share.rs | 22 +- vortex-layout/src/plan/physical/plans/take.rs | 41 ++-- .../src/plan/physical/plans/zoned.rs | 59 +++-- vortex-layout/src/plan/physical/selection.rs | 4 +- vortex-layout/src/plan/physical/slots.rs | 205 ++++++++++++++++++ vortex-layout/src/plan/physical/tests.rs | 94 +++++--- vortex-layout/src/plan/physical/typed.rs | 106 +++++---- vortex-layout/src/plan/physical/vtable.rs | 39 ++-- vortex-layout/src/scan/execution/filter.rs | 4 +- 29 files changed, 774 insertions(+), 620 deletions(-) delete mode 100644 vortex-layout/src/plan/physical/children.rs create mode 100644 vortex-layout/src/plan/physical/slots.rs diff --git a/docs/developer-guide/internals/scan-planning.md b/docs/developer-guide/internals/scan-planning.md index e03131924ac..710d8f86147 100644 --- a/docs/developer-guide/internals/scan-planning.md +++ b/docs/developer-guide/internals/scan-planning.md @@ -7,16 +7,15 @@ over a row domain, describing the reads and derived work needed to produce that Plan operators describe *what work happens*, not *which layout produced it*. Their identity and operator-specific state are independent of the source layout kind. The complete plan node is not: -its common lazy-child container can own hidden source state used to materialize individual children -on demand. +each of its lazy child slots can own hidden source state used to materialize that child on demand. | Operator | Work | | --- | --- | | `SegmentScan` | read one segment and decode it to an array | | `Concat` | concatenate its children row-wise | -| `Pack` | assemble a struct from one child per field, plus optional validity | +| `Pack` | assemble a struct from an optional validity slot, then one slot per field | | `Take` | index `values` by `codes` | -| `ListPack` | assemble a list from elements and offsets, plus optional validity | +| `ListPack` | assemble a list from elements, offsets, and an optional validity slot | | `Eval` | apply an expression to its child | | `RowIdx` | offset row numbers into the file's row domain | @@ -28,12 +27,25 @@ The stored layout tree describes all physical data in a file. A plan is query-sp from that tree for one projection, filter, and row domain. Different queries over the same file can therefore produce different plans. +## Slots + +A plan's children live in fixed-position slots, as an array's do. Each operator defines what every +slot position holds, and an absent child is an empty slot rather than a missing entry, so the +positions of the other children never depend on which are present. A non-nullable `Pack` has an +empty validity slot 0 and its fields from slot 1; a pruning `Zoned` plan has an empty data slot and +its zones in slot 1. + +Lowering maps a layout's slots to plan slots in one place, and most operators use the layout's +own slot positions. Compiling a plan into a pipeline is the one place slots become children: +pipelines have no absent inputs, so a plan that joins its children compiles its present slots in +slot order, and inlet `i` of its source reads the `i`-th present slot. + ## Optimization -Child replacement is implemented by the common plan container rather than by every operator. It -replaces the external child container, clones `PlanData`, then invokes the operator's -`PlanVTable::with_children` callback to validate the new children and refresh derived caches such -as `Concat` row offsets. Rules therefore rewrite the generic tree without reconstructing common +Slot replacement is implemented by the common plan container rather than by every operator. It +replaces the external slots, clones `PlanData`, then invokes the operator's +`PlanVTable::with_slots` callback to validate the new slots and refresh derived caches such as +`Concat` row offsets. Rules therefore rewrite the generic tree without reconstructing common plan fields inside each operator. Optimization rewrites the initial tree so that each expression is evaluated as close as possible to @@ -47,7 +59,7 @@ later execution stage will perform. Each operator is a small vtable type implementing `PlanVTable`, paired with a `Plan` container over a shared `PlanRef`. `PlanRef` points to one allocation whose ordinary fields hold the operator -ID, dtype, row count, and lazy children. Only the unsized tail containing the vtable and +ID, dtype, row count, and lazy child slots. Only the unsized tail containing the vtable and `V::PlanData` is erased behind `dyn DynPlan`, so common-field reads do not use dynamic dispatch. `Plan` provides typed access to that operator data through `Deref`. diff --git a/vortex-layout/src/plan/physical/children.rs b/vortex-layout/src/plan/physical/children.rs deleted file mode 100644 index bfa1541b87c..00000000000 --- a/vortex-layout/src/plan/physical/children.rs +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -use std::fmt; -use std::sync::Arc; - -use once_cell::sync::OnceCell; -use vortex_error::VortexResult; -use vortex_error::vortex_err; - -use crate::plan::physical::PlanRef; - -type ChildInitializer = dyn Fn(usize) -> VortexResult + 'static + Send + Sync; - -/// Ordered plan children that may be initialized one slot at a time. -/// -/// Eagerly constructed operators store already-filled slots. Layout lowering instead installs an -/// initializer that owns the source layout and lowers each child on first access. -#[derive(Clone)] -pub struct PlanChildren { - initializer: Option>, - cache: Arc<[OnceCell]>, -} - -impl PlanChildren { - /// Creates lazy child slots backed by `initializer`. - pub(crate) fn lazy( - len: usize, - initializer: impl Fn(usize) -> VortexResult + 'static + Send + Sync, - ) -> Self { - Self { - initializer: Some(Arc::new(initializer)), - cache: (0..len).map(|_| OnceCell::new()).collect::>().into(), - } - } - - /// Returns the number of children without initializing any slot. - pub fn len(&self) -> usize { - self.cache.len() - } - - /// Returns whether there are no children. - pub fn is_empty(&self) -> bool { - self.cache.is_empty() - } - - /// Returns a child, initializing and caching its slot on first access. - pub fn get(&self, index: usize) -> VortexResult> { - self.get_ref(index).map(|child| child.cloned()) - } - - /// Borrows a child, initializing and caching its slot on first access. - pub(crate) fn get_ref(&self, index: usize) -> VortexResult> { - let Some(cell) = self.cache.get(index) else { - return Ok(None); - }; - if let Some(child) = cell.get() { - return Ok(Some(child)); - } - - let initializer = self - .initializer - .as_ref() - .ok_or_else(|| vortex_err!("Plan child {index} was not initialized"))?; - Ok(Some(cell.get_or_try_init(|| initializer(index))?)) - } - - /// Iterates over the children in logical order, initializing slots as they are visited. - pub fn iter(&self) -> impl ExactSizeIterator> + '_ { - (0..self.len()).map(|index| { - self.get(index)? - .ok_or_else(|| vortex_err!("Plan child {index} is absent")) - }) - } - - /// Borrows the children in logical order, initializing slots as they are visited. - pub(crate) fn iter_refs(&self) -> impl ExactSizeIterator> + '_ { - (0..self.len()).map(|index| { - self.get_ref(index)? - .ok_or_else(|| vortex_err!("Plan child {index} is absent")) - }) - } - - /// Materializes all children into an eager vector. - pub fn to_vec(&self) -> VortexResult> { - self.iter().collect() - } -} - -impl From> for PlanChildren { - fn from(children: Vec) -> Self { - let cache = children - .into_iter() - .map(OnceCell::with_value) - .collect::>() - .into(); - Self { - initializer: None, - cache, - } - } -} - -impl From<[PlanRef; N]> for PlanChildren { - fn from(children: [PlanRef; N]) -> Self { - Vec::from(children).into() - } -} - -impl Default for PlanChildren { - fn default() -> Self { - Vec::new().into() - } -} - -impl fmt::Debug for PlanChildren { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("PlanChildren") - .field("len", &self.len()) - .field( - "initialized", - &self - .cache - .iter() - .filter(|slot| slot.get().is_some()) - .count(), - ) - .finish() - } -} diff --git a/vortex-layout/src/plan/physical/display.rs b/vortex-layout/src/plan/physical/display.rs index 8a05f184761..ced4ef89048 100644 --- a/vortex-layout/src/plan/physical/display.rs +++ b/vortex-layout/src/plan/physical/display.rs @@ -108,11 +108,10 @@ impl TreeDisplayAdapter for PlanTreeDisplay<'_> { plan: &PlanRef, visit: &mut dyn FnMut(&str, &PlanRef, bool) -> fmt::Result, ) -> fmt::Result { - let children = plan.children(); - for index in 0..children.len() { - let child = plan.child_required(index).map_err(|_| fmt::Error)?; - let child_name = plan.child_name(index); - visit(child_name.as_ref(), &child, index + 1 == children.len())?; + let mut present = plan.slots().present().peekable(); + while let Some((slot, lazy)) = present.next() { + let child = lazy.get().map_err(|_| fmt::Error)?; + visit(&plan.slot_name(slot), child, present.peek().is_none())?; } Ok(()) } diff --git a/vortex-layout/src/plan/physical/lower.rs b/vortex-layout/src/plan/physical/lower.rs index 1ea284cc1ca..f496a175914 100644 --- a/vortex-layout/src/plan/physical/lower.rs +++ b/vortex-layout/src/plan/physical/lower.rs @@ -16,22 +16,20 @@ use crate::layouts::dict::Dict; use crate::layouts::dict::DictLayout; use crate::layouts::flat::Flat; use crate::layouts::flat::FlatLayout; -use crate::layouts::list::ELEMENTS_CHILD_INDEX; use crate::layouts::list::List; use crate::layouts::list::ListLayout; -use crate::layouts::list::OFFSETS_CHILD_INDEX; -use crate::layouts::list::VALIDITY_CHILD_INDEX; use crate::layouts::struct_::Struct; use crate::layouts::struct_::StructLayout; use crate::layouts::zoned::LegacyStats; use crate::layouts::zoned::Zoned; use crate::plan::physical::ConcatPlan; use crate::plan::physical::FilterPlan; +use crate::plan::physical::LazyPlanRef; use crate::plan::physical::LegacyStatsPlan; use crate::plan::physical::ListPackPlan; use crate::plan::physical::PackPlan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::SegmentScanPlan; use crate::plan::physical::TakePlan; use crate::plan::physical::ZonedPlan; @@ -42,8 +40,8 @@ use crate::plan::physical::ZonedPlan; /// data, and the optimizer rewrites a statistics predicate over them into a pruning plan. Those /// with no zones or no statistics lower to their data. /// -/// The root operator is built immediately. Its child container owns a hidden clone of the source -/// layout and lowers each child independently on first access. +/// The root operator is built immediately. Each of its slots owns a hidden clone of the source +/// layout and lowers its child independently on first access. /// /// A flat layout lowers to a [`Filter`](crate::plan::physical::Filter) over its segment scan, so /// the scan returns only the rows it is executed with. @@ -94,31 +92,25 @@ fn lower_chunked(layout: &ChunkedLayout) -> VortexResult { // SAFETY: Chunked layout construction validates that every child has the parent dtype and // that their row counts sum to the parent; offsets were computed with checked addition above. Ok(unsafe { - ConcatPlan::from_children_unchecked( + ConcatPlan::from_slots_unchecked( layout.dtype().clone(), layout.row_count(), row_offsets.into(), - lazy_children(layout.to_layout(), (0..layout.nchildren()).collect()), + lazy_slots(&layout.to_layout()), ) }) } fn lower_struct(layout: &StructLayout) -> VortexResult { - // Struct layout slot 0 is validity and field i is slot i + 1. The plan puts validity last so - // field indices are identical to their plan-child indices. - let fields = layout.struct_fields().clone(); - let mut slots = (1..=fields.nfields()).collect::>(); - if layout.dtype().is_nullable() { - slots.push(0); - } - // SAFETY: Struct layout construction validates child count and row counts, and its logical - // slot schema defines every field dtype plus the optional non-nullable boolean validity dtype. + // SAFETY: Struct layout construction validates child count and row counts. Its slots are + // validity then one per field, which are the Pack slots, and its logical slot schema defines + // every field dtype plus the optional non-nullable boolean validity dtype. Ok(unsafe { - PackPlan::from_children_unchecked( - fields, + PackPlan::from_slots_unchecked( + layout.struct_fields().clone(), layout.dtype().nullability(), layout.row_count(), - lazy_children(layout.to_layout(), slots), + lazy_slots(&layout.to_layout()), ) }) } @@ -129,41 +121,51 @@ fn lower_dict(layout: &DictLayout) -> VortexResult { // SAFETY: Dict layout construction validates its values and codes slots. The plan reorders // those slots to [codes, values] while preserving the derived output dtype and row domain. Ok(unsafe { - TakePlan::from_children_unchecked( + TakePlan::from_slots_unchecked( layout.dtype().clone(), layout.row_count(), - lazy_children(layout.to_layout(), vec![1, 0]), + lazy_slots_from(&layout.to_layout(), [1, 0]), ) }) } fn lower_list(layout: &ListLayout) -> VortexResult { - let mut slots = vec![ELEMENTS_CHILD_INDEX, OFFSETS_CHILD_INDEX]; - if layout.dtype().is_nullable() { - slots.push(VALIDITY_CHILD_INDEX); - } // SAFETY: List layout construction validates its element, offsets, and optional validity - // child shapes before this plan preserves them in the same logical order. + // child shapes. Its slots are `[elements, offsets, validity]`, which are the ListPack slots. Ok(unsafe { - ListPackPlan::from_children_unchecked( + ListPackPlan::from_slots_unchecked( layout.dtype().clone(), layout.row_count(), - lazy_children(layout.to_layout(), slots), + lazy_slots(&layout.to_layout()), ) }) } -fn lazy_children(layout: LayoutRef, slots: Vec) -> PlanChildren { - PlanChildren::lazy(slots.len(), move |index| { - let slot = slots - .get(index) - .copied() - .ok_or_else(|| vortex_err!("Missing plan child slot {index}"))?; - let child = layout - .slot(slot)? - .ok_or_else(|| vortex_err!("Layout child slot {slot} is absent"))?; - lower(&child) - }) +/// One lazy plan slot per slot of `layout`, at the same position. +fn lazy_slots(layout: &LayoutRef) -> PlanSlots { + lazy_slots_from(layout, 0..layout.nslots()) +} + +/// One lazy plan slot per entry of `layout_slots`: plan slot `i` lowers layout slot +/// `layout_slots[i]`. +/// +/// This is where every lowering maps layout slots to plan slots. An absent layout slot is an +/// absent plan slot; presence comes from the layout's metadata, so nothing is read or lowered +/// until a plan slot is first accessed. +fn lazy_slots_from(layout: &LayoutRef, layout_slots: impl IntoIterator) -> PlanSlots { + layout_slots + .into_iter() + .map(|slot| { + layout.slot_type(slot)?; + let layout = Arc::clone(layout); + Some(LazyPlanRef::lazy(move || { + let child = layout + .slot(slot)? + .ok_or_else(|| vortex_err!("Layout child slot {slot} is absent"))?; + lower(&child) + })) + }) + .collect() } fn lower_zoned(layout: &LayoutRef) -> VortexResult { @@ -186,22 +188,22 @@ fn lower_zoned(layout: &LayoutRef) -> VortexResult { .ok_or_else(|| vortex_err!("Zoned layout has no data child"))?; return lower(&data); } - let children = lazy_children(Arc::clone(layout), vec![0, 1]); + let slots = lazy_slots_from(layout, [0, 1]); let zone_len = u64::try_from(metadata.zone_len())?; // A legacy table is converted to the aggregate schema when the plan is optimized. Ok(match metadata.legacy_stats() { - Some(stats) => LegacyStatsPlan::from_children( + Some(stats) => LegacyStatsPlan::from_slots( layout.dtype().clone(), layout.row_count(), - children, + slots, zone_len, Arc::clone(stats), ) .into_plan(), - None => ZonedPlan::from_children( + None => ZonedPlan::from_slots( layout.dtype().clone(), layout.row_count(), - children, + slots, zone_len, metadata.aggregate_fns(), ) diff --git a/vortex-layout/src/plan/physical/mod.rs b/vortex-layout/src/plan/physical/mod.rs index bedd3a7cad8..ae4819ef361 100644 --- a/vortex-layout/src/plan/physical/mod.rs +++ b/vortex-layout/src/plan/physical/mod.rs @@ -5,12 +5,11 @@ //! //! A plan is a tree of physical operators over a row domain. Operator identity and //! operator-specific state do not depend on the source layout kind, so rewrites can reason about a -//! plan's shape alone. The common child container can initialize individual slots lazily. +//! plan's shape alone. Children live in fixed-position slots, each of which can be built lazily. //! //! [`operator`] defines the execution contracts, `operators` implements the concrete sources //! and stages, and [`pipeline`] compiles and runs them. -mod children; mod display; mod lower; pub mod operator; @@ -20,10 +19,10 @@ pub mod optimizer; pub mod pipeline; mod plans; pub(crate) mod selection; +mod slots; mod typed; mod vtable; -pub use children::PlanChildren; pub use display::PlanIndentedFormatter; pub use display::PlanSummaryExtractor; pub use display::PlanTreeContext; @@ -65,30 +64,15 @@ pub use plans::ZonedData; pub use plans::ZonedPlan; pub use plans::plan_row_idx_expression; pub use plans::row_idx_dtype; +pub use slots::LazyPlanRef; +pub use slots::PlanSlots; pub use typed::DynPlan; pub use typed::Plan; pub(crate) use typed::PlanByPtr; pub use typed::PlanParts; pub use typed::PlanRef; -use vortex_error::VortexResult; -use vortex_error::vortex_bail; pub use vtable::PlanId; pub use vtable::PlanVTable; -/// Returns an error when `children` does not have exactly `expected` entries. -pub(crate) fn check_child_count( - name: &str, - children: &PlanChildren, - expected: usize, -) -> VortexResult<()> { - if children.len() != expected { - vortex_bail!( - "{name} expects {expected} children but got {}", - children.len() - ); - } - Ok(()) -} - #[cfg(test)] mod tests; diff --git a/vortex-layout/src/plan/physical/operators/pack.rs b/vortex-layout/src/plan/physical/operators/pack.rs index 4fefa368f05..0ac601f58e2 100644 --- a/vortex-layout/src/plan/physical/operators/pack.rs +++ b/vortex-layout/src/plan/physical/operators/pack.rs @@ -31,7 +31,9 @@ enum InletState { Ended, } -/// Zips its inlets, one per field and the validity last when nullable, into structs. +/// Zips its inlets, the validity first when nullable and then one per field, into structs. +/// +/// The inlets are the present slots of a [`Pack`](crate::plan::physical::Pack) plan in slot order. /// /// Readiness updates only the inlets whose sinks changed them. Computing takes the shortest /// front batch's length from every inlet, retaining the remainder of longer batches. @@ -102,21 +104,22 @@ impl Operator for PackSource { .ok_or_else(|| vortex_err!("Pack computed before every inlet was ready"))?; len = len.min(front.len()); } - let mut arrays = Vec::with_capacity(self.states.len()); - for index in 0..self.states.len() { + let inlets = self.states.len(); + let nullable = self.nullable; + let mut take = |index: usize| -> VortexResult { let mut inlet = cx.inlet(index); - arrays.push(take_rows(&mut inlet, len)?); + let array = take_rows(&mut inlet, len)?; self.observe(index, &inlet); - } - let validity = if self.nullable { - Validity::Array( - arrays - .pop() - .ok_or_else(|| vortex_err!("Nullable Pack is missing its validity"))?, - ) + Ok(array) + }; + let validity = if nullable { + Validity::Array(take(0)?) } else { Validity::NonNullable }; + let arrays = (usize::from(nullable)..inlets) + .map(&mut take) + .collect::>>()?; let array = assemble(&self.fields, arrays, validity).into_array(); Ok(Step::Final(array)) } diff --git a/vortex-layout/src/plan/physical/optimize.rs b/vortex-layout/src/plan/physical/optimize.rs index 272fbacc7b8..3a20dc196f7 100644 --- a/vortex-layout/src/plan/physical/optimize.rs +++ b/vortex-layout/src/plan/physical/optimize.rs @@ -3,14 +3,16 @@ //! Plan optimization. //! -//! The optimizer applies static rewrites top-down, optimizes children, then retries rewrites -//! exposed by the optimized children. +//! The optimizer applies static rewrites top-down, optimizes the children in every present slot, +//! then retries rewrites exposed by the optimized children. use vortex_error::VortexResult; use vortex_session::VortexSession; use crate::plan::physical::Concat; +use crate::plan::physical::LazyPlanRef; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::optimizer::reduce_parent; use crate::plan::physical::optimizer::reduce_plan; @@ -18,8 +20,8 @@ fn reduce(plan: &PlanRef, session: &VortexSession) -> VortexResult VortexResult return optimize(rewritten, session); } - let mut children: Option> = None; - for (index, child) in plan.children().iter_refs().enumerate() { - let child = child?; - let optimized = optimize(child.clone(), session)?; - if let Some(children) = &mut children { - children.push(optimized); - } else if !PlanRef::ptr_eq(child, &optimized) { - let mut optimized_children = Vec::with_capacity(plan.child_count()); - for previous in plan.children().iter_refs().take(index) { - optimized_children.push(previous?.clone()); + // Absent slots stay absent, and unchanged slots keep their shared lazy slot. + let mut changed = false; + let slots = plan + .slots() + .iter() + .map(|slot| { + let Some(lazy) = slot else { + return Ok(None); + }; + let child = lazy.get()?; + let optimized = optimize(child.clone(), session)?; + if PlanRef::ptr_eq(child, &optimized) { + return Ok(Some(lazy.clone())); } - optimized_children.push(optimized); - children = Some(optimized_children); - } - } - - let Some(children) = children else { + changed = true; + Ok(Some(LazyPlanRef::ready(optimized))) + }) + .collect::>()?; + if !changed { return Ok(plan); - }; + } - let plan = plan.with_children(children)?; + let plan = plan.with_slots(slots)?; if let Some(rewritten) = reduce(&plan, session)? { return optimize(rewritten, session); } diff --git a/vortex-layout/src/plan/physical/optimizer/mod.rs b/vortex-layout/src/plan/physical/optimizer/mod.rs index adf38185b6b..d7ab19bb829 100644 --- a/vortex-layout/src/plan/physical/optimizer/mod.rs +++ b/vortex-layout/src/plan/physical/optimizer/mod.rs @@ -80,14 +80,12 @@ pub(crate) fn reduce_plan( PLAN_RULES.evaluate(plan, session) } -/// Attempts a static rewrite for `parent` and its child at `child_idx`. +/// Attempts a static rewrite for `parent` and `child`, its child in `slot`. pub(crate) fn reduce_parent( + child: &PlanRef, parent: &PlanRef, - child_idx: usize, + slot: usize, session: &VortexSession, ) -> VortexResult> { - let Some(child) = parent.children().get_ref(child_idx)? else { - return Ok(None); - }; - PARENT_RULES.evaluate(child, parent, child_idx, session) + PARENT_RULES.evaluate(child, parent, slot, session) } diff --git a/vortex-layout/src/plan/physical/optimizer/rules.rs b/vortex-layout/src/plan/physical/optimizer/rules.rs index 192dd8e194b..2ded82403dd 100644 --- a/vortex-layout/src/plan/physical/optimizer/rules.rs +++ b/vortex-layout/src/plan/physical/optimizer/rules.rs @@ -134,7 +134,7 @@ pub trait PlanParentReduceRule: Debug + Send + Sync + 'static { /// The concrete parent operator matched by this rule. type Parent: PlanVTable; - /// Attempts to replace `parent` based on its child at `child_idx`. + /// Attempts to replace `parent` based on its child in slot `child_idx`. fn reduce_parent( &self, child: &Plan, @@ -149,7 +149,7 @@ pub trait DynPlanParentReduceRule: Debug + Send + Sync + 'static { /// Returns whether this rule supports the concrete child and parent operators. fn matches(&self, child: &PlanRef, parent: &PlanRef) -> bool; - /// Attempts to replace `parent` based on `child` at `child_idx`. + /// Attempts to replace `parent` based on `child` in slot `child_idx`. fn reduce_parent( &self, child: &PlanRef, diff --git a/vortex-layout/src/plan/physical/pipeline/scan.rs b/vortex-layout/src/plan/physical/pipeline/scan.rs index 816c3566d7e..930c48bc731 100644 --- a/vortex-layout/src/plan/physical/pipeline/scan.rs +++ b/vortex-layout/src/plan/physical/pipeline/scan.rs @@ -749,7 +749,7 @@ impl Scan { }; if self.next == self.splits.len() { // Compiled operators own what they need. The last split no longer needs the - // lowering tree or the source layouts retained by its lazy children. + // lowering tree or the source layouts retained by its lazy slots. self.root = None; } if output.is_some() { diff --git a/vortex-layout/src/plan/physical/pipeline/synthetic.rs b/vortex-layout/src/plan/physical/pipeline/synthetic.rs index 82c4f5bc3f6..ca584a310f6 100644 --- a/vortex-layout/src/plan/physical/pipeline/synthetic.rs +++ b/vortex-layout/src/plan/physical/pipeline/synthetic.rs @@ -27,10 +27,10 @@ use vortex_session::registry::CachedId; use super::Chain; use super::Compiler; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; use crate::plan::physical::operator::Cx; use crate::plan::physical::operator::Input; @@ -79,7 +79,7 @@ impl SyntheticPlan { vtable: Synthetic, dtype, row_count, - children: children.into(), + slots: children.into(), data: SyntheticData { builder: Arc::new(builder), }, @@ -101,9 +101,9 @@ impl PlanVTable for Synthetic { None } - fn with_children( + fn with_slots( _plan: &Plan, - _children: &PlanChildren, + _slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { Ok(()) diff --git a/vortex-layout/src/plan/physical/pipeline/tests.rs b/vortex-layout/src/plan/physical/pipeline/tests.rs index a4da689e016..c0788a1070d 100644 --- a/vortex-layout/src/plan/physical/pipeline/tests.rs +++ b/vortex-layout/src/plan/physical/pipeline/tests.rs @@ -30,6 +30,7 @@ use vortex_array::expr::gt; use vortex_array::expr::lit; use vortex_array::expr::root; use vortex_array::serde::SerializeOptions; +use vortex_array::validity::Validity; use vortex_buffer::Alignment; use vortex_buffer::Buffer; use vortex_buffer::ByteBufferMut; @@ -51,8 +52,8 @@ use crate::layouts::struct_::StructLayout; use crate::plan::physical::ConcatPlan; use crate::plan::physical::EvalPlan; use crate::plan::physical::Filter; +use crate::plan::physical::LazyPlanRef; use crate::plan::physical::PackPlan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanRef; use crate::plan::physical::SegmentScan; use crate::plan::physical::Take; @@ -521,6 +522,45 @@ fn pack_splits_misaligned_fields(#[case] order: &[u32]) -> VortexResult<()> { assert_view(&expected, &(0..ROWS), &mask, run.arrays) } +/// A nullable struct's validity is its first slot, so it is the pack's first inlet, ahead of +/// fields chunked differently from it. +#[test] +fn nullable_pack_reads_validity_from_its_first_slot() -> VortexResult<()> { + let mut store = Store::default(); + let validity = BoolArray::from_iter((0..ROWS).map(|v| v % 3 != 0)).into_array(); + let a = PrimitiveArray::from_iter(0..ROWS as i32).into_array(); + let b = PrimitiveArray::from_iter((0..ROWS as i64).map(|v| v * 10)).into_array(); + let expected = StructArray::try_new( + ["a", "b"].into(), + [a.clone(), b.clone()], + ROWS as usize, + Validity::Array(validity.clone()), + )? + .into_array(); + let layout = StructLayout::new( + ROWS, + expected.dtype().clone(), + vec![ + store.chunked(&validity, &[4, 16])?, + store.chunked(&a, &[7, 5, 8])?, + store.flat(&b)?, + ], + ) + .into_layout(); + let plan = lower(&layout)?; + let rows = 2..18; + let mask = Mask::from_iter((0..16).map(|v| v % 4 != 1)); + + let run = run( + &store, + &plan, + rows.clone(), + mask.clone(), + delivery(Delivery::Lifo), + )?; + assert_view(&expected, &rows, &mask, run.arrays) +} + /// Pack emits as soon as every inlet has a batch, without waiting for an inlet to fill. #[test] fn pack_emits_when_every_field_has_data() -> VortexResult<()> { @@ -913,15 +953,20 @@ fn bound_selection_never_initializes_skipped_chunks() -> VortexResult<()> { let array = PrimitiveArray::from_iter(0..20i32).into_array(); let first = lower(&store.flat(&array.slice(0..5)?)?)?; let last = lower(&store.flat(&array.slice(15..20)?)?)?; - let children = PlanChildren::lazy(4, move |index| match index { - 0 => Ok(first.clone()), - 3 => Ok(last.clone()), - _ => Err(vortex_err!("unselected chunk {index} was initialized")), - }); + let children = (0..4) + .map(|index| { + let (first, last) = (first.clone(), last.clone()); + Some(LazyPlanRef::lazy(move || match index { + 0 => Ok(first.clone()), + 3 => Ok(last.clone()), + _ => Err(vortex_err!("unselected chunk {index} was initialized")), + })) + }) + .collect(); // SAFETY: All four logical chunks have five rows of the same primitive dtype. The two // unselected initializers deliberately fail to detect discovery as well as compilation. let plan = unsafe { - ConcatPlan::from_children_unchecked( + ConcatPlan::from_slots_unchecked( array.dtype().clone(), 20, vec![0, 5, 10, 15].into(), diff --git a/vortex-layout/src/plan/physical/plans/concat.rs b/vortex-layout/src/plan/physical/plans/concat.rs index b12e25ea173..be0f9706f5f 100644 --- a/vortex-layout/src/plan/physical/plans/concat.rs +++ b/vortex-layout/src/plan/physical/plans/concat.rs @@ -10,18 +10,18 @@ use vortex_array::dtype::DType; use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_error::vortex_ensure_eq; -use vortex_error::vortex_err; use vortex_mask::Mask; use vortex_session::VortexSession; use vortex_session::registry::CachedId; use crate::plan::physical::Eval; use crate::plan::physical::EvalPlan; +use crate::plan::physical::LazyPlanRef; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; use crate::plan::physical::operators::ConcatSource; use crate::plan::physical::optimizer::PlanParentReduceRule; @@ -44,23 +44,25 @@ pub struct ConcatData { pub type ConcatPlan = Plan; impl ConcatPlan { - /// Creates a concatenation from potentially unresolved children without validation. + /// Creates a concatenation from potentially unbuilt slots, one per chunk, without + /// validation. /// /// # Safety /// - /// Every child must have `dtype`; `row_offsets` must contain the cumulative row offset of - /// every child; and the sum of all child row counts must equal `row_count` without overflow. - pub(crate) unsafe fn from_children_unchecked( + /// Every slot must be present with `dtype`; `row_offsets` must contain the cumulative row + /// offset of every chunk; and the sum of all chunk row counts must equal `row_count` without + /// overflow. + pub(crate) unsafe fn from_slots_unchecked( dtype: DType, row_count: u64, row_offsets: Arc<[u64]>, - children: PlanChildren, + slots: PlanSlots, ) -> Self { PlanParts { vtable: Concat, dtype, row_count, - children, + slots, data: ConcatData { row_offsets }, } .into_typed() @@ -86,7 +88,7 @@ impl ConcatPlan { } // SAFETY: Child dtypes and the checked cumulative row metadata were validated above. Ok(unsafe { - Self::from_children_unchecked(dtype, row_count, row_offsets.into(), children.into()) + Self::from_slots_unchecked(dtype, row_count, row_offsets.into(), children.into()) }) } @@ -102,23 +104,29 @@ impl ConcatPlan { dtype: DType, map: impl Fn(PlanRef) -> VortexResult + Send + Sync + 'static, ) -> Self { - let children = self.children().clone(); - let child_dtype = dtype.clone(); + let map = Arc::new(map); + let slots = self + .slots() + .iter() + .map(|slot| { + let chunk = slot.clone()?; + let map = Arc::clone(&map); + let dtype = dtype.clone(); + Some(LazyPlanRef::lazy(move || { + let chunk = chunk.get()?; + let mapped = map(chunk.clone())?; + vortex_ensure_eq!(mapped.dtype(), &dtype); + vortex_ensure_eq!(mapped.row_count(), chunk.row_count()); + Ok(mapped) + })) + }) + .collect(); PlanParts { vtable: Concat, dtype, row_count: self.row_count(), data: self.data().clone(), - children: PlanChildren::lazy(self.children().len(), move |index| { - let child = children - .get(index)? - .ok_or_else(|| vortex_err!("Missing chunk {index}"))?; - let rows = child.row_count(); - let mapped = map(child)?; - vortex_ensure_eq!(mapped.dtype(), &child_dtype); - vortex_ensure_eq!(mapped.row_count(), rows); - Ok(mapped) - }), + slots, } .into_typed() } @@ -138,23 +146,17 @@ impl PlanVTable for Concat { Some(EmptyMetadata) } - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, data: &mut Self::PlanData, ) -> VortexResult<()> { - if children.len() != plan.children().len() { - vortex_bail!( - "Concat expects {} children but got {}", - plan.children().len(), - children.len() - ); - } + slots.check_len("Concat", plan.slots().len())?; - let mut row_offsets = Vec::with_capacity(children.len()); + let mut row_offsets = Vec::with_capacity(slots.len()); let mut row_count = 0u64; - for child in children.iter() { - let child = child?; + for index in 0..slots.len() { + let child = slots.required(index)?; if child.dtype() != plan.dtype() { vortex_bail!( "Concat child dtype {} does not match {}", @@ -177,8 +179,8 @@ impl PlanVTable for Concat { Ok(()) } - fn child_name(_plan: &Plan, index: usize) -> Cow<'_, str> { - Cow::Owned(format!("chunks[{index}]")) + fn slot_name(_plan: &Plan, slot: usize) -> Cow<'_, str> { + Cow::Owned(format!("chunks[{slot}]")) } fn compile( @@ -201,7 +203,7 @@ impl PlanVTable for Concat { ) else { continue; }; - let chunk = plan.child_required(index)?; + let chunk = plan.slot_required(index)?; if let Some(chain) = compiler.compile( &chunk, rows.start + span.start as u64 - start..rows.start + span.end as u64 - start, diff --git a/vortex-layout/src/plan/physical/plans/eval.rs b/vortex-layout/src/plan/physical/plans/eval.rs index 043f0676198..22623b18a49 100644 --- a/vortex-layout/src/plan/physical/plans/eval.rs +++ b/vortex-layout/src/plan/physical/plans/eval.rs @@ -22,12 +22,11 @@ use vortex_session::registry::CachedId; use crate::plan::physical::PackPlan; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; -use crate::plan::physical::check_child_count; use crate::plan::physical::operators::EvalStage; use crate::plan::physical::optimizer::PlanParentReduceRule; use crate::plan::physical::optimizer::PlanReduceRule; @@ -66,7 +65,7 @@ impl EvalPlan { vtable: Eval, dtype: expression.dtype().clone(), row_count: child.row_count(), - children: vec![child].into(), + slots: [child].into(), data: EvalData { expression }, } .into_typed() @@ -79,7 +78,7 @@ impl EvalPlan { /// Returns the child plan supplying the expression root. pub fn child_plan(&self) -> VortexResult { - self.child_required(0) + self.slot_required(0) } } @@ -101,16 +100,14 @@ impl PlanVTable for Eval { None } - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { - check_child_count("Eval", children, 1)?; - let child = children - .get(0)? - .ok_or_else(|| vortex_error::vortex_err!("Eval child is absent"))?; - validate_expression_child(plan.expression(), &child)?; + slots.check_len("Eval", 1)?; + let child = slots.required(0)?; + validate_expression_child(plan.expression(), child)?; if child.row_count() != plan.row_count() { vortex_error::vortex_bail!( "Eval child has {} rows but the plan has {}", @@ -121,11 +118,11 @@ impl PlanVTable for Eval { Ok(()) } - fn child_name(_plan: &Plan, index: usize) -> Cow<'_, str> { + fn slot_name(_plan: &Plan, index: usize) -> Cow<'_, str> { if index == 0 { Cow::Borrowed("child") } else { - Cow::Owned(format!("child[{index}]")) + Cow::Owned(format!("slot[{index}]")) } } @@ -210,7 +207,7 @@ impl PlanReduceRule for EvalPackRule { }; let fields = StructFields::new(expr.names.clone(), vec![child_expr.dtype().clone()]); - let child_plan = plan.child(0)?.vortex_expect("no child plan"); + let child_plan = plan.slot(0)?.vortex_expect("no child plan"); let child_plan = EvalPlan::try_new(child_expr.clone(), child_plan)?.into_plan(); let plan = PackPlan::try_new( diff --git a/vortex-layout/src/plan/physical/plans/filter.rs b/vortex-layout/src/plan/physical/plans/filter.rs index 857a69a376e..7513788d032 100644 --- a/vortex-layout/src/plan/physical/plans/filter.rs +++ b/vortex-layout/src/plan/physical/plans/filter.rs @@ -7,18 +7,16 @@ use std::ops::Range; use vortex_array::EmptyMetadata; use vortex_error::VortexResult; use vortex_error::vortex_bail; -use vortex_error::vortex_err; use vortex_mask::Mask; use vortex_session::registry::CachedId; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; use crate::plan::physical::SegmentScan; -use crate::plan::physical::check_child_count; use crate::plan::physical::operators::MaskStage; use crate::plan::physical::pipeline::Chain; use crate::plan::physical::pipeline::Compiler; @@ -45,7 +43,7 @@ impl FilterPlan { vtable: Filter, dtype: child.dtype().clone(), row_count: child.row_count(), - children: vec![child].into(), + slots: [child].into(), data: FilterData, } .into_typed() @@ -53,7 +51,7 @@ impl FilterPlan { /// Returns the plan whose rows are filtered. pub fn child_plan(&self) -> VortexResult { - self.child_required(0) + self.slot_required(0) } } @@ -70,26 +68,24 @@ impl PlanVTable for Filter { Some(EmptyMetadata) } - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { - check_child_count("Filter", children, 1)?; - let child = children - .get(0)? - .ok_or_else(|| vortex_err!("Filter child is absent"))?; + slots.check_len("Filter", 1)?; + let child = slots.required(0)?; if child.dtype() != plan.dtype() || child.row_count() != plan.row_count() { vortex_bail!("Filter child does not match the filter's dtype and row count"); } Ok(()) } - fn child_name(_plan: &Plan, index: usize) -> Cow<'_, str> { + fn slot_name(_plan: &Plan, index: usize) -> Cow<'_, str> { if index == 0 { Cow::Borrowed("child") } else { - Cow::Owned(format!("child[{index}]")) + Cow::Owned(format!("slot[{index}]")) } } diff --git a/vortex-layout/src/plan/physical/plans/legacy_stats.rs b/vortex-layout/src/plan/physical/plans/legacy_stats.rs index 8aa0e897da7..99b8da2b6cd 100644 --- a/vortex-layout/src/plan/physical/plans/legacy_stats.rs +++ b/vortex-layout/src/plan/physical/plans/legacy_stats.rs @@ -10,7 +10,6 @@ use vortex_array::dtype::DType; use vortex_array::expr::stats::Stat; use vortex_error::VortexResult; use vortex_error::vortex_bail; -use vortex_error::vortex_err; use vortex_mask::Mask; use vortex_session::VortexSession; use vortex_session::registry::CachedId; @@ -19,14 +18,13 @@ use crate::layouts::zoned::legacy_stats_to_aggregates; use crate::plan::physical::Eval; use crate::plan::physical::EvalPlan; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; use crate::plan::physical::Zoned; use crate::plan::physical::ZonedPlan; -use crate::plan::physical::check_child_count; use crate::plan::physical::optimizer::PlanParentReduceRule; use crate::plan::physical::optimizer::PlanReduceRule; use crate::plan::physical::pipeline::Chain; @@ -58,10 +56,10 @@ pub struct LegacyStatsData { pub type LegacyStatsPlan = Plan; impl LegacyStatsPlan { - pub(crate) fn from_children( + pub(crate) fn from_slots( dtype: DType, row_count: u64, - children: PlanChildren, + slots: PlanSlots, zone_len: u64, stats: Arc<[Stat]>, ) -> Self { @@ -69,7 +67,7 @@ impl LegacyStatsPlan { vtable: LegacyStats, dtype, row_count, - children, + slots, data: LegacyStatsData { zone_len, stats }, } .into_typed() @@ -80,7 +78,7 @@ impl LegacyStatsPlan { pub fn new(data: PlanRef, zones: PlanRef, zone_len: u64, stats: Arc<[Stat]>) -> Self { let dtype = data.dtype().clone(); let row_count = data.row_count(); - Self::from_children(dtype, row_count, vec![data, zones].into(), zone_len, stats) + Self::from_slots(dtype, row_count, [data, zones].into(), zone_len, stats) } /// Returns the number of rows each zone summarises. @@ -95,12 +93,12 @@ impl LegacyStatsPlan { /// Returns the plan producing the summarised data. pub fn data_plan(&self) -> VortexResult { - self.child_required(DATA) + self.slot_required(DATA) } /// Returns the plan producing the legacy zone table. pub fn zones_plan(&self) -> VortexResult { - self.child_required(ZONES) + self.slot_required(ZONES) } } @@ -117,26 +115,24 @@ impl PlanVTable for LegacyStats { None } - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { - check_child_count("LegacyStats", children, 2)?; - let data = children - .get(DATA)? - .ok_or_else(|| vortex_err!("LegacyStats data child is absent"))?; + slots.check_len("LegacyStats", 2)?; + let data = slots.required(DATA)?; if data.dtype() != plan.dtype() || data.row_count() != plan.row_count() { vortex_bail!("LegacyStats data child shape does not match the plan output"); } Ok(()) } - fn child_name(_plan: &Plan, index: usize) -> Cow<'_, str> { + fn slot_name(_plan: &Plan, index: usize) -> Cow<'_, str> { match index { DATA => Cow::Borrowed("data"), ZONES => Cow::Borrowed("zones"), - _ => Cow::Owned(format!("child[{index}]")), + _ => Cow::Owned(format!("slot[{index}]")), } } diff --git a/vortex-layout/src/plan/physical/plans/list_pack.rs b/vortex-layout/src/plan/physical/plans/list_pack.rs index bb075c7f4e2..7f43d4618a0 100644 --- a/vortex-layout/src/plan/physical/plans/list_pack.rs +++ b/vortex-layout/src/plan/physical/plans/list_pack.rs @@ -13,17 +13,18 @@ use vortex_error::vortex_err; use vortex_session::registry::CachedId; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; const ELEMENTS: usize = 0; const OFFSETS: usize = 1; const VALIDITY: usize = 2; -/// Assembles a list from elements and offsets, plus an optional trailing validity child. +/// Assembles a list from slots `[elements, offsets, validity]`, with validity present exactly +/// when the list is nullable. #[derive(Clone, Debug)] pub struct ListPack; @@ -35,23 +36,24 @@ pub struct ListPackData; pub type ListPackPlan = Plan; impl ListPackPlan { - /// Creates a list assembly from potentially unresolved children without validation. + /// Creates a list assembly from potentially unbuilt slots without validation. /// /// # Safety /// - /// `dtype` must be a list whose element dtype matches the elements child. The offsets child - /// must be a non-nullable integer with `row_count + 1` rows. A non-nullable boolean validity - /// child with `row_count` rows must be present exactly when `dtype` is nullable. - pub(crate) unsafe fn from_children_unchecked( + /// `slots` must be `[elements, offsets, validity]`. `dtype` must be a list whose element + /// dtype matches the elements child. The offsets child must be a non-nullable integer with + /// `row_count + 1` rows. A non-nullable boolean validity child with `row_count` rows must be + /// present exactly when `dtype` is nullable. + pub(crate) unsafe fn from_slots_unchecked( dtype: DType, row_count: u64, - children: PlanChildren, + slots: PlanSlots, ) -> Self { PlanParts { vtable: ListPack, dtype, row_count, - children, + slots, data: ListPackData, } .into_typed() @@ -69,28 +71,28 @@ impl ListPackPlan { validity: Option, ) -> VortexResult { let dtype = DType::List(Arc::new(elements.dtype().clone()), nullability); - let mut children = vec![elements, offsets]; - children.extend(validity); - let children = PlanChildren::from(children); - validate_children(&dtype, row_count, &children)?; + let slots = [Some(elements), Some(offsets), validity] + .into_iter() + .collect(); + validate_slots(&dtype, row_count, &slots)?; - // SAFETY: All child shape invariants were validated above. - Ok(unsafe { Self::from_children_unchecked(dtype, row_count, children) }) + // SAFETY: All slot shape invariants were validated above. + Ok(unsafe { Self::from_slots_unchecked(dtype, row_count, slots) }) } /// Returns the plan producing list elements. pub fn elements(&self) -> VortexResult { - self.child_required(ELEMENTS) + self.slot_required(ELEMENTS) } /// Returns the plan producing list offsets. pub fn offsets(&self) -> VortexResult { - self.child_required(OFFSETS) + self.slot_required(OFFSETS) } /// Returns the plan producing list validity, if the list is nullable. pub fn validity(&self) -> VortexResult> { - self.child(VALIDITY) + self.slot(VALIDITY) } } @@ -108,39 +110,31 @@ impl PlanVTable for ListPack { Some(EmptyMetadata) } - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { - validate_children(plan.dtype(), plan.row_count(), children) + validate_slots(plan.dtype(), plan.row_count(), slots) } - fn child_name(_plan: &Plan, index: usize) -> Cow<'_, str> { - match index { + fn slot_name(_plan: &Plan, slot: usize) -> Cow<'_, str> { + match slot { ELEMENTS => Cow::Borrowed("elements"), OFFSETS => Cow::Borrowed("offsets"), VALIDITY => Cow::Borrowed("validity"), - _ => Cow::Owned(format!("child[{index}]")), + _ => Cow::Owned(format!("slot[{slot}]")), } } } -fn validate_children(dtype: &DType, row_count: u64, children: &PlanChildren) -> VortexResult<()> { +fn validate_slots(dtype: &DType, row_count: u64, slots: &PlanSlots) -> VortexResult<()> { let elements_dtype = dtype .as_list_element_opt() .ok_or_else(|| vortex_err!("ListPack output dtype must be a list, got {dtype}"))?; - let expected_children = 2 + usize::from(dtype.is_nullable()); - if children.len() != expected_children { - vortex_bail!( - "ListPack expects {expected_children} children but got {}", - children.len() - ); - } + slots.check_len("ListPack", VALIDITY + 1)?; - let elements = children - .get(ELEMENTS)? - .ok_or_else(|| vortex_err!("ListPack elements child is absent"))?; + let elements = slots.required(ELEMENTS)?; if elements.dtype() != elements_dtype.as_ref() { vortex_bail!( "ListPack elements child has dtype {} but the list element dtype is {}", @@ -149,9 +143,7 @@ fn validate_children(dtype: &DType, row_count: u64, children: &PlanChildren) -> ); } - let offsets = children - .get(OFFSETS)? - .ok_or_else(|| vortex_err!("ListPack offsets child is absent"))?; + let offsets = slots.required(OFFSETS)?; if !offsets.dtype().is_int() || offsets.dtype().is_nullable() { vortex_bail!( "ListPack offsets child must have a non-nullable integer dtype, got {}", @@ -168,23 +160,25 @@ fn validate_children(dtype: &DType, row_count: u64, children: &PlanChildren) -> ); } - if dtype.is_nullable() { - let validity = children - .get(VALIDITY)? - .ok_or_else(|| vortex_err!("ListPack validity child is absent"))?; - let validity_dtype = DType::Bool(Nullability::NonNullable); - if validity.dtype() != &validity_dtype { - vortex_bail!( - "ListPack validity child has dtype {} but must have dtype {validity_dtype}", - validity.dtype() - ); - } - if validity.row_count() != row_count { - vortex_bail!( - "ListPack validity child has {} rows but the plan has {row_count}", - validity.row_count() - ); + match (dtype.is_nullable(), slots.get(VALIDITY)?) { + (true, Some(validity)) => { + let validity_dtype = DType::Bool(Nullability::NonNullable); + if validity.dtype() != &validity_dtype { + vortex_bail!( + "ListPack validity child has dtype {} but must have dtype {validity_dtype}", + validity.dtype() + ); + } + if validity.row_count() != row_count { + vortex_bail!( + "ListPack validity child has {} rows but the plan has {row_count}", + validity.row_count() + ); + } } + (false, None) => {} + (true, None) => vortex_bail!("Nullable ListPack has no validity child"), + (false, Some(_)) => vortex_bail!("Non-nullable ListPack has a validity child"), } Ok(()) } diff --git a/vortex-layout/src/plan/physical/plans/pack.rs b/vortex-layout/src/plan/physical/plans/pack.rs index d3886de19ad..e27e23e211e 100644 --- a/vortex-layout/src/plan/physical/plans/pack.rs +++ b/vortex-layout/src/plan/physical/plans/pack.rs @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors use std::borrow::Cow; +use std::iter; use std::ops::Range; use vortex_array::EmptyMetadata; @@ -35,10 +36,10 @@ use vortex_session::registry::CachedId; use crate::plan::physical::Eval; use crate::plan::physical::EvalPlan; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; use crate::plan::physical::operators::OnceSource; use crate::plan::physical::operators::PackSource; @@ -48,10 +49,21 @@ use crate::plan::physical::optimizer::PlanParentReduceRule; use crate::plan::physical::pipeline::Chain; use crate::plan::physical::pipeline::Compiler; -/// Assembles a struct from one child per field, plus an optional trailing validity child. +/// Assembles a struct from a validity slot followed by one slot per field. +/// +/// The slots match those of a struct array and a struct layout: slot +/// [`VALIDITY_SLOT`](Self::VALIDITY_SLOT) holds the validity, present exactly when the struct is +/// nullable, and field `i` is in slot `FIELDS_OFFSET + i`. #[derive(Clone, Debug)] pub struct Pack; +impl Pack { + /// The slot holding struct validity, absent when the struct is non-nullable. + pub const VALIDITY_SLOT: usize = 0; + /// The slot of field zero; field `i` is in slot `FIELDS_OFFSET + i`. + pub const FIELDS_OFFSET: usize = 1; +} + /// Operator-specific struct assembly data. #[derive(Clone, Debug)] pub struct PackData; @@ -60,24 +72,25 @@ pub struct PackData; pub type PackPlan = Plan; impl PackPlan { - /// Creates a struct assembly from potentially unresolved children without validation. + /// Creates a struct assembly from potentially unbuilt slots without validation. /// /// # Safety /// - /// `children` must contain one child per field, followed by a non-nullable boolean validity - /// child exactly when `nullability` is nullable. Every child must have `row_count` rows, and - /// every field child must have its corresponding field dtype. - pub(crate) unsafe fn from_children_unchecked( + /// `slots` must hold a validity slot followed by one slot per field. The validity slot must + /// be present exactly when `nullability` is nullable and produce non-nullable booleans. Every + /// field slot must be present with its field dtype, and every child must have `row_count` + /// rows. + pub(crate) unsafe fn from_slots_unchecked( fields: StructFields, nullability: Nullability, row_count: u64, - children: PlanChildren, + slots: PlanSlots, ) -> Self { PlanParts { vtable: Pack, dtype: DType::Struct(fields, nullability), row_count, - children, + slots, data: PackData, } .into_typed() @@ -136,10 +149,11 @@ impl PackPlan { field_plans: Vec, validity: Option, ) -> Self { - let mut children = field_plans; - children.extend(validity); - // SAFETY: The caller guarantees the same child invariants required by this constructor. - unsafe { Self::from_children_unchecked(fields, nullability, row_count, children.into()) } + let slots = iter::once(validity) + .chain(field_plans.into_iter().map(Some)) + .collect(); + // SAFETY: The caller guarantees the same slot invariants required by this constructor. + unsafe { Self::from_slots_unchecked(fields, nullability, row_count, slots) } } /// Returns the struct fields assembled by this plan. @@ -156,7 +170,12 @@ impl PackPlan { /// Returns the plan producing struct validity, if the struct is nullable. pub fn validity(&self) -> VortexResult> { - self.child(self.nfields()) + self.slot(Pack::VALIDITY_SLOT) + } + + /// Returns the plan producing field `index`. + pub fn field(&self, index: usize) -> VortexResult { + self.slot_required(Pack::FIELDS_OFFSET + index) } } @@ -174,44 +193,34 @@ impl PlanVTable for Pack { Some(EmptyMetadata) } - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { - let expected_children = plan.nfields() + usize::from(plan.dtype().is_nullable()); - if children.len() != expected_children { - vortex_bail!( - "Pack expects {expected_children} children but got {}", - children.len() - ); + slots.check_len("Pack", Pack::FIELDS_OFFSET + plan.nfields())?; + match (plan.dtype().is_nullable(), slots.get(Pack::VALIDITY_SLOT)?) { + (true, Some(validity)) => validate_validity_child(plan.row_count(), validity)?, + (false, None) => {} + (true, None) => vortex_bail!("Nullable Pack has no validity child"), + (false, Some(_)) => vortex_bail!("Non-nullable Pack has a validity child"), } - for (index, field_dtype) in plan.fields().fields().enumerate() { - let child = children - .get(index)? - .ok_or_else(|| vortex_err!("Pack field child {index} is absent"))?; - validate_field_child(index, &field_dtype, plan.row_count(), &child)?; - } - if plan.dtype().is_nullable() { - let validity = children - .get(plan.nfields())? - .ok_or_else(|| vortex_err!("Pack validity child is absent"))?; - validate_validity_child(plan.row_count(), &validity)?; + let child = slots.required(Pack::FIELDS_OFFSET + index)?; + validate_field_child(index, &field_dtype, plan.row_count(), child)?; } Ok(()) } - fn child_name(plan: &Plan, index: usize) -> Cow<'_, str> { - assert!( - index < plan.children().len(), - "Pack child index out of bounds: {index} of {}", - plan.children().len() - ); - if let Some(name) = plan.fields().field_name(index) { - return Cow::Borrowed(name.as_ref()); + fn slot_name(plan: &Plan, slot: usize) -> Cow<'_, str> { + if slot == Pack::VALIDITY_SLOT { + return Cow::Borrowed("validity"); } - Cow::Borrowed("validity") + let name = plan + .fields() + .field_name(slot - Pack::FIELDS_OFFSET) + .vortex_expect("Pack slot index out of bounds"); + Cow::Borrowed(name.as_ref()) } fn compile( @@ -220,7 +229,8 @@ impl PlanVTable for Pack { mask: &Mask, compiler: &mut Compiler<'_>, ) -> VortexResult> { - let count = plan.children().len(); + // Inlet `i` reads the `i`-th present slot: validity first when nullable, then the fields. + let count = plan.slots().present().count(); if count == 0 { // A struct with no fields still has rows, so no child can carry them. let array = empty_struct( @@ -234,11 +244,11 @@ impl PlanVTable for Pack { return Ok(None); } let mut chains = Vec::with_capacity(count); - for child in plan.children().iter_refs() { + for child in plan.children() { chains.push( compiler .compile(child?, rows.clone(), mask)? - .ok_or_else(|| vortex_err!("A Pack field produced no rows"))?, + .ok_or_else(|| vortex_err!("A Pack child produced no rows"))?, ); } let nullable = plan.dtype().is_nullable(); @@ -501,8 +511,7 @@ fn field_name(fields: &StructFields, index: usize) -> VortexResult { } fn field_plan(plan: &Plan, index: usize) -> VortexResult { - plan.child(index)? - .ok_or_else(|| vortex_err!("Struct field {index} has no plan")) + plan.field(index) } fn expanded_struct_root( diff --git a/vortex-layout/src/plan/physical/plans/row_idx.rs b/vortex-layout/src/plan/physical/plans/row_idx.rs index 5c45d2cae4f..725cde429e5 100644 --- a/vortex-layout/src/plan/physical/plans/row_idx.rs +++ b/vortex-layout/src/plan/physical/plans/row_idx.rs @@ -29,12 +29,11 @@ use crate::layouts::row_idx::RowIdx as RowIdxFn; use crate::plan::physical::EvalPlan; use crate::plan::physical::PackPlan; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; -use crate::plan::physical::check_child_count; use crate::plan::physical::operators::OnceSource; use crate::plan::physical::pipeline::Chain; use crate::plan::physical::pipeline::Compiler; @@ -61,7 +60,7 @@ impl RowIdxPlan { vtable: RowIdx, dtype: row_idx_dtype(), row_count, - children: PlanChildren::default(), + slots: PlanSlots::default(), data: RowIdxData, } .into_typed() @@ -86,12 +85,12 @@ impl PlanVTable for RowIdx { Some(EmptyMetadata) } - fn with_children( + fn with_slots( _plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { - check_child_count("RowIdx", children, 0) + slots.check_len("RowIdx", 0) } fn compile( diff --git a/vortex-layout/src/plan/physical/plans/segment_scan.rs b/vortex-layout/src/plan/physical/plans/segment_scan.rs index a07ff0e8b4d..afc72a410fe 100644 --- a/vortex-layout/src/plan/physical/plans/segment_scan.rs +++ b/vortex-layout/src/plan/physical/plans/segment_scan.rs @@ -12,11 +12,10 @@ use vortex_session::registry::CachedId; use vortex_session::registry::ReadContext; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; -use crate::plan::physical::check_child_count; use crate::plan::physical::pipeline::Chain; use crate::plan::physical::pipeline::Compiler; use crate::segments::SegmentId; @@ -49,7 +48,7 @@ impl SegmentScanPlan { vtable: SegmentScan, dtype, row_count, - children: PlanChildren::default(), + slots: PlanSlots::default(), data: SegmentScanData { segment_id, array_ctx, @@ -89,12 +88,12 @@ impl PlanVTable for SegmentScan { None } - fn with_children( + fn with_slots( _plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { - check_child_count("SegmentScan", children, 0)?; + slots.check_len("SegmentScan", 0)?; Ok(()) } diff --git a/vortex-layout/src/plan/physical/plans/share.rs b/vortex-layout/src/plan/physical/plans/share.rs index 96166e7c81f..030ae549cbe 100644 --- a/vortex-layout/src/plan/physical/plans/share.rs +++ b/vortex-layout/src/plan/physical/plans/share.rs @@ -9,18 +9,16 @@ use vortex_array::EmptyMetadata; use vortex_array::IntoArray; use vortex_error::VortexResult; use vortex_error::vortex_bail; -use vortex_error::vortex_err; use vortex_mask::Mask; use vortex_session::registry::CachedId; use crate::arrays::shared::Shared; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; -use crate::plan::physical::check_child_count; use crate::plan::physical::operators::OnceSource; use crate::plan::physical::operators::UNBOUNDED; use crate::plan::physical::operators::WholeSource; @@ -51,7 +49,7 @@ impl SharePlan { vtable: Share, dtype: child.dtype().clone(), row_count: child.row_count(), - children: vec![child].into(), + slots: [child].into(), data: ShareData, } .into_typed() @@ -59,7 +57,7 @@ impl SharePlan { /// Returns the shared plan. pub fn child_plan(&self) -> VortexResult { - self.child_required(0) + self.slot_required(0) } } @@ -76,26 +74,24 @@ impl PlanVTable for Share { Some(EmptyMetadata) } - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { - check_child_count("Share", children, 1)?; - let child = children - .get(0)? - .ok_or_else(|| vortex_err!("Share child is absent"))?; + slots.check_len("Share", 1)?; + let child = slots.required(0)?; if child.dtype() != plan.dtype() || child.row_count() != plan.row_count() { vortex_bail!("Share child does not match the share's dtype and row count"); } Ok(()) } - fn child_name(_plan: &Plan, index: usize) -> Cow<'_, str> { + fn slot_name(_plan: &Plan, index: usize) -> Cow<'_, str> { if index == 0 { Cow::Borrowed("child") } else { - Cow::Owned(format!("child[{index}]")) + Cow::Owned(format!("slot[{index}]")) } } diff --git a/vortex-layout/src/plan/physical/plans/take.rs b/vortex-layout/src/plan/physical/plans/take.rs index 81a3026d815..8c426be1197 100644 --- a/vortex-layout/src/plan/physical/plans/take.rs +++ b/vortex-layout/src/plan/physical/plans/take.rs @@ -22,13 +22,12 @@ use vortex_session::registry::CachedId; use crate::plan::physical::Eval; use crate::plan::physical::EvalPlan; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; use crate::plan::physical::Share; -use crate::plan::physical::check_child_count; use crate::plan::physical::operators::OnceSource; use crate::plan::physical::operators::TakeSource; use crate::plan::physical::optimizer::PlanParentReduceRule; @@ -38,7 +37,7 @@ use crate::plan::physical::pipeline::Compiler; const CODES: usize = 0; const VALUES: usize = 1; -/// Indexes `values` by `codes`, with children ordered as `[codes, values]`. +/// Indexes `values` by `codes`, with slots `[codes, values]`. #[derive(Clone, Debug)] pub struct Take; @@ -46,22 +45,22 @@ pub struct Take; pub type TakePlan = Plan; impl TakePlan { - /// Creates a take from potentially unresolved children without validation. + /// Creates a take from potentially unbuilt slots without validation. /// /// # Safety /// - /// `children` must be `[codes, values]`; `codes` must have `row_count` rows; and `dtype` must - /// be the values dtype unioned with the codes nullability. - pub(crate) unsafe fn from_children_unchecked( + /// `slots` must be `[codes, values]`, both present; `codes` must have `row_count` rows; and + /// `dtype` must be the values dtype unioned with the codes nullability. + pub(crate) unsafe fn from_slots_unchecked( dtype: DType, row_count: u64, - children: PlanChildren, + slots: PlanSlots, ) -> Self { PlanParts { vtable: Take, dtype, row_count, - children, + slots, data: (), } .into_typed() @@ -79,7 +78,7 @@ impl TakePlan { vtable: Take, dtype, row_count, - children: vec![codes, values].into(), + slots: [codes, values].into(), data: (), } .into_typed() @@ -87,12 +86,12 @@ impl TakePlan { /// Returns the plan producing indices. pub fn codes(&self) -> VortexResult { - self.child_required(CODES) + self.slot_required(CODES) } /// Returns the plan producing the values being indexed. pub fn values(&self) -> VortexResult { - self.child_required(VALUES) + self.slot_required(VALUES) } } @@ -109,18 +108,14 @@ impl PlanVTable for Take { Some(EmptyMetadata) } - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, _data: &mut Self::PlanData, ) -> VortexResult<()> { - check_child_count("Take", children, 2)?; - let codes = children - .get(CODES)? - .ok_or_else(|| vortex_error::vortex_err!("Take codes child is absent"))?; - let values = children - .get(VALUES)? - .ok_or_else(|| vortex_error::vortex_err!("Take values child is absent"))?; + slots.check_len("Take", 2)?; + let codes = slots.required(CODES)?; + let values = slots.required(VALUES)?; let dtype = values .dtype() .union_nullability(codes.dtype().nullability()); @@ -130,11 +125,11 @@ impl PlanVTable for Take { Ok(()) } - fn child_name(_plan: &Plan, index: usize) -> Cow<'_, str> { + fn slot_name(_plan: &Plan, index: usize) -> Cow<'_, str> { match index { CODES => Cow::Borrowed("codes"), VALUES => Cow::Borrowed("values"), - _ => Cow::Owned(format!("child[{index}]")), + _ => Cow::Owned(format!("slot[{index}]")), } } diff --git a/vortex-layout/src/plan/physical/plans/zoned.rs b/vortex-layout/src/plan/physical/plans/zoned.rs index 15bf8cd7dfd..01f9a3b525e 100644 --- a/vortex-layout/src/plan/physical/plans/zoned.rs +++ b/vortex-layout/src/plan/physical/plans/zoned.rs @@ -36,12 +36,11 @@ use vortex_session::registry::CachedId; use crate::layouts::zoned::zone_map::ZoneMap; use crate::plan::physical::Eval; use crate::plan::physical::Plan; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; use crate::plan::physical::PlanParts; use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; -use crate::plan::physical::check_child_count; use crate::plan::physical::operators::OnceSource; use crate::plan::physical::operators::UNBOUNDED; use crate::plan::physical::operators::WholeSource; @@ -152,8 +151,8 @@ pub struct ZonedData { /// Its zone table has the aggregate schema of `vortex.zoned` layouts; legacy `vortex.stats` /// layouts reach it through [`LegacyStats`](crate::plan::physical::LegacyStats), which converts /// their tables. An expression containing abstract statistic functions can rewrite it into a -/// pruning plan that retains only the zone-statistics child; a plan that reads the data is -/// optimized to its data child alone. +/// pruning plan whose data slot is absent, retaining only the zone-statistics child; a plan that +/// reads the data is optimized to its data child alone. Its slots are always `[data, zones]`. #[derive(Clone, Debug)] pub struct Zoned; @@ -161,10 +160,10 @@ pub struct Zoned; pub type ZonedPlan = Plan; impl ZonedPlan { - pub(crate) fn from_children( + pub(crate) fn from_slots( dtype: DType, row_count: u64, - children: PlanChildren, + slots: PlanSlots, zone_len: u64, aggregate_fns: Arc<[AggregateFnRef]>, ) -> Self { @@ -172,7 +171,7 @@ impl ZonedPlan { vtable: Zoned, dtype, row_count, - children, + slots, data: ZonedData { zone_len, aggregate_fns, @@ -192,10 +191,10 @@ impl ZonedPlan { ) -> Self { let dtype = data.dtype().clone(); let row_count = data.row_count(); - Self::from_children( + Self::from_slots( dtype, row_count, - vec![data, zones].into(), + [data, zones].into(), zone_len, aggregate_fns, ) @@ -229,16 +228,12 @@ impl ZonedPlan { /// Returns the plan producing the summarised data, unless this is a pruning plan. pub fn data_plan(&self) -> VortexResult> { - if self.is_pruning() { - return Ok(None); - } - self.child(DATA) + self.slot(DATA) } /// Returns the plan producing the zone statistics. pub fn zones_plan(&self) -> VortexResult { - let index = if self.is_pruning() { 0 } else { ZONES }; - self.child_required(index) + self.slot_required(ZONES) } /// Returns whether this plan represents a zone-backed pruning proof. @@ -266,7 +261,7 @@ impl ZonedPlan { vtable: Zoned, dtype: expression.dtype().clone(), row_count: self.row_count(), - children: vec![self.zones_plan()?].into(), + slots: [None, self.slots()[ZONES].clone()].into_iter().collect(), data, } .into_typed(), @@ -294,13 +289,19 @@ impl PlanVTable for Zoned { None } - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, data: &mut Self::PlanData, ) -> VortexResult<()> { + slots.check_len("Zoned", ZONES + 1)?; + if slots[ZONES].is_none() { + vortex_bail!("Zoned plan has no zones child"); + } if plan.is_pruning() { - check_child_count("Zoned pruning", children, 1)?; + if slots[DATA].is_some() { + vortex_bail!("Zoned pruning plan has a data child"); + } // New zones may prove different zones, so the proof of the old ones is not kept. if let Some(state) = &mut data.pruning { state.pruned_zones = Arc::new(PrunedZones::new(&state.expression)); @@ -308,28 +309,18 @@ impl PlanVTable for Zoned { return Ok(()); } - check_child_count("Zoned", children, 2)?; - let data = children - .get(DATA)? - .ok_or_else(|| vortex_error::vortex_err!("Zoned data child is absent"))?; + let data = slots.required(DATA)?; if data.dtype() != plan.dtype() || data.row_count() != plan.row_count() { - vortex_error::vortex_bail!("Zoned data child shape does not match the plan output"); + vortex_bail!("Zoned data child shape does not match the plan output"); } Ok(()) } - fn child_name(plan: &Plan, index: usize) -> Cow<'_, str> { - if plan.is_pruning() { - return if index == 0 { - Cow::Borrowed("zones") - } else { - Cow::Owned(format!("child[{index}]")) - }; - } - match index { + fn slot_name(_plan: &Plan, slot: usize) -> Cow<'_, str> { + match slot { DATA => Cow::Borrowed("data"), ZONES => Cow::Borrowed("zones"), - _ => Cow::Owned(format!("child[{index}]")), + _ => Cow::Owned(format!("slot[{slot}]")), } } diff --git a/vortex-layout/src/plan/physical/selection.rs b/vortex-layout/src/plan/physical/selection.rs index 1087ad61538..eb3e5422c72 100644 --- a/vortex-layout/src/plan/physical/selection.rs +++ b/vortex-layout/src/plan/physical/selection.rs @@ -38,7 +38,7 @@ pub(crate) fn selected(plan: PlanRef, rows: Range, mask: Mask) -> PlanRef { vtable: Selected, dtype: plan.dtype().clone(), row_count: plan.row_count(), - children: vec![plan].into(), + slots: [plan].into(), data: SelectedData { rows, mask }, } .into_typed() @@ -69,7 +69,7 @@ impl PlanVTable for Selected { if mask.all_false() { return Ok(None); } - compiler.compile(&plan.child_required(0)?, rows, &mask) + compiler.compile(&plan.slot_required(0)?, rows, &mask) } } diff --git a/vortex-layout/src/plan/physical/slots.rs b/vortex-layout/src/plan/physical/slots.rs new file mode 100644 index 00000000000..5a9ae1b97d2 --- /dev/null +++ b/vortex-layout/src/plan/physical/slots.rs @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +use std::fmt; +use std::ops::Deref; +use std::sync::Arc; + +use once_cell::sync::OnceCell; +use smallvec::SmallVec; +use vortex_error::VortexResult; +use vortex_error::vortex_bail; +use vortex_error::vortex_err; + +use crate::plan::physical::PlanRef; + +type SlotInitializer = dyn Fn() -> VortexResult + 'static + Send + Sync; + +/// A plan child that may be built on first access. +/// +/// Clones share the built plan, so a child is built at most once however many plans hold its +/// slot. A failed build leaves the slot unbuilt, and the next access retries it. +#[derive(Clone)] +pub struct LazyPlanRef(Arc>); + +/// The built plan and its initializer, in one allocation. +struct LazyPlan { + cell: OnceCell, + init: F, // must be last for unsized coercion +} + +impl LazyPlanRef { + /// A slot built by `init` on first access. + pub fn lazy(init: impl Fn() -> VortexResult + 'static + Send + Sync) -> Self { + Self(Arc::new(LazyPlan { + cell: OnceCell::new(), + init, + })) + } + + /// A slot holding the already-built `plan`. + pub fn ready(plan: PlanRef) -> Self { + Self(Arc::new(LazyPlan { + cell: OnceCell::with_value(plan), + init: || Err(vortex_err!("A built plan slot was initialized again")), + })) + } + + /// Returns the plan, building it on first access. + pub fn get(&self) -> VortexResult<&PlanRef> { + self.0.cell.get_or_try_init(|| (self.0.init)()) + } + + /// Returns whether the plan has been built. + pub fn is_initialized(&self) -> bool { + self.0.cell.get().is_some() + } +} + +impl From for LazyPlanRef { + fn from(plan: PlanRef) -> Self { + Self::ready(plan) + } +} + +impl fmt::Debug for LazyPlanRef { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0.cell.get() { + Some(plan) => write!(formatter, "{plan}"), + None => formatter.write_str(""), + } + } +} + +/// The fixed-position child slots of a plan. +/// +/// Each operator defines what every slot position holds. A `None` slot is an absent child, such +/// as the validity of a non-nullable struct, so the positions of the other slots never shift. +#[derive(Clone, Default)] +pub struct PlanSlots(SmallVec<[Option; 4]>); + +impl PlanSlots { + /// Creates empty slots with room for `capacity` slots. + pub fn with_capacity(capacity: usize) -> Self { + Self(SmallVec::with_capacity(capacity)) + } + + /// Appends a slot. + pub fn push(&mut self, slot: Option) { + self.0.push(slot); + } + + /// Returns the plan in `slot`, building it on first access, or `None` when the slot is + /// absent. Errors when `slot` is out of bounds. + pub fn get(&self, slot: usize) -> VortexResult> { + match self.0.get(slot) { + Some(Some(lazy)) => lazy.get().map(Some), + Some(None) => Ok(None), + None => vortex_bail!( + "Plan slot {slot} is out of bounds for {} slots", + self.0.len() + ), + } + } + + /// Returns the plan in `slot`, or an error when the slot is absent or out of bounds. + pub fn required(&self, slot: usize) -> VortexResult<&PlanRef> { + self.get(slot)? + .ok_or_else(|| vortex_err!("Plan slot {slot} is absent")) + } + + /// The present slots with their positions, in slot order, without building any child. + /// + /// This is the one place absent slots are dropped. Everything that walks a plan's children + /// goes through it, with the slot position when it needs one. + pub fn present(&self) -> impl Iterator + '_ { + self.0 + .iter() + .enumerate() + .filter_map(|(slot, lazy)| Some((slot, lazy.as_ref()?))) + } + + /// The plans of the present slots in slot order, building each as it is visited. + /// + /// Pipelines have no absent inputs, so compiling a plan's children uses this: the `i`-th + /// child is the `i`-th present slot. + pub fn children(&self) -> impl Iterator> + '_ { + self.present().map(|(_, lazy)| lazy.get()) + } + + /// Returns an error when there are not exactly `expected` slots. + pub(crate) fn check_len(&self, name: &str, expected: usize) -> VortexResult<()> { + if self.0.len() != expected { + vortex_bail!("{name} expects {expected} slots but got {}", self.0.len()); + } + Ok(()) + } +} + +impl Deref for PlanSlots { + type Target = [Option]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromIterator> for PlanSlots { + fn from_iter>>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} + +/// Built slots, with `None` for an absent child. +impl FromIterator> for PlanSlots { + fn from_iter>>(iter: I) -> Self { + Self( + iter.into_iter() + .map(|plan| plan.map(LazyPlanRef::ready)) + .collect(), + ) + } +} + +impl From>> for PlanSlots { + fn from(slots: Vec>) -> Self { + Self(SmallVec::from_vec(slots)) + } +} + +/// Built slots, every one present. +impl From> for PlanSlots { + fn from(children: Vec) -> Self { + // `PlanRef` and `Option` have the same layout, so this collect reuses the + // vector's buffer, and `from_vec` keeps it when the slots do not fit inline. + let slots: Vec> = children + .into_iter() + .map(|child| Some(LazyPlanRef::ready(child))) + .collect(); + Self(SmallVec::from_vec(slots)) + } +} + +/// Built slots, every one present, without a temporary vector. +impl From<[PlanRef; N]> for PlanSlots { + fn from(children: [PlanRef; N]) -> Self { + children.into_iter().map(Some).collect() + } +} + +impl fmt::Debug for PlanSlots { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("PlanSlots") + .field("len", &self.0.len()) + .field("present", &self.present().count()) + .field( + "built", + &self + .present() + .filter(|(_, lazy)| lazy.is_initialized()) + .count(), + ) + .finish() + } +} diff --git a/vortex-layout/src/plan/physical/tests.rs b/vortex-layout/src/plan/physical/tests.rs index 76486c935ef..3ecd7cd2b04 100644 --- a/vortex-layout/src/plan/physical/tests.rs +++ b/vortex-layout/src/plan/physical/tests.rs @@ -100,9 +100,8 @@ fn make_row_idx_plan(expression: Expression, child: PlanRef) -> VortexResult VortexResult { - plan.child(index)? - .ok_or_else(|| vortex_err!("missing child {index}")) +fn child_of(plan: &PlanRef, slot: usize) -> VortexResult { + plan.slot_required(slot) } fn assert_unsupported(error: vortex_error::VortexError) { @@ -133,8 +132,8 @@ fn flat_plan_is_a_filtered_segment_scan_without_children() -> VortexResult<()> { assert!(plan.is::()); let scan = child_of(&plan, 0)?; assert!(scan.is::()); - assert_eq!(scan.child_count(), 0); - assert!(scan.child(0)?.is_none()); + assert_eq!(scan.nslots(), 0); + assert!(scan.slot(0).is_err()); Ok(()) } @@ -150,7 +149,7 @@ fn chunked_plan_exposes_chunks() -> VortexResult<()> { let plan = make_plan(layout)?; assert!(plan.is::()); - assert_eq!(plan.child_count(), 2); + assert_eq!(plan.nslots(), 2); assert_eq!(child_of(&plan, 0)?.row_count(), 2); assert_eq!(child_of(&plan, 1)?.row_count(), 1); Ok(()) @@ -215,9 +214,9 @@ fn struct_plan_lowers_each_field_on_access() -> VortexResult<()> { .into_layout(); let plan = make_plan(layout)?; - assert_eq!(child_of(&plan, 0)?.row_count(), 1); + assert_eq!(child_of(&plan, Pack::FIELDS_OFFSET)?.row_count(), 1); assert_unsupported( - child_of(&plan, 1) + child_of(&plan, Pack::FIELDS_OFFSET + 1) .err() .ok_or_else(|| vortex_err!("unsupported field unexpectedly produced a plan"))?, ); @@ -236,14 +235,14 @@ fn dict_plan_orders_codes_before_values() -> VortexResult<()> { let plan = make_plan(layout)?; assert!(plan.is::()); - assert_eq!(plan.child_count(), 2); + assert_eq!(plan.nslots(), 2); assert_eq!(child_of(&plan, 0)?.dtype(), &codes_dtype); assert_eq!(child_of(&plan, 1)?.dtype(), &values_dtype); Ok(()) } #[test] -fn list_plan_appends_validity_when_nullable() -> VortexResult<()> { +fn list_plan_validity_slot_is_present_when_nullable() -> VortexResult<()> { let element_dtype = primitive(PType::I32, Nullability::NonNullable); let offsets_dtype = primitive(PType::U32, Nullability::NonNullable); let non_nullable = ListLayout::new( @@ -256,9 +255,10 @@ fn list_plan_appends_validity_when_nullable() -> VortexResult<()> { let plan = make_plan(non_nullable)?; assert!(plan.is::()); - assert_eq!(plan.child_count(), 2); + assert_eq!(plan.nslots(), 3); assert_eq!(child_of(&plan, 0)?.dtype(), &element_dtype); assert_eq!(child_of(&plan, 1)?.dtype(), &offsets_dtype); + assert!(plan.slot(2)?.is_none()); let nullable = ListLayout::new( DType::List(Arc::new(element_dtype.clone()), Nullability::Nullable), @@ -268,7 +268,7 @@ fn list_plan_appends_validity_when_nullable() -> VortexResult<()> { ) .into_layout(); let nullable_plan = make_plan(nullable)?; - assert_eq!(nullable_plan.child_count(), 3); + assert_eq!(nullable_plan.nslots(), 3); assert_eq!( child_of(&nullable_plan, 2)?.dtype(), &DType::Bool(Nullability::NonNullable) @@ -277,7 +277,7 @@ fn list_plan_appends_validity_when_nullable() -> VortexResult<()> { } #[test] -fn struct_plan_appends_validity_when_nullable() -> VortexResult<()> { +fn struct_plan_validity_slot_is_present_when_nullable() -> VortexResult<()> { let field_dtype = primitive(PType::I32, Nullability::NonNullable); let fields = StructFields::from_iter([("a", field_dtype.clone()), ("b", field_dtype.clone())]); let non_nullable = StructLayout::new( @@ -292,9 +292,13 @@ fn struct_plan_appends_validity_when_nullable() -> VortexResult<()> { let plan = make_plan(non_nullable)?; assert!(plan.is::()); - assert_eq!(plan.child_count(), 2); - assert_eq!(child_of(&plan, 0)?.dtype(), &field_dtype); - assert_eq!(child_of(&plan, 1)?.dtype(), &field_dtype); + assert_eq!(plan.nslots(), 3); + assert!(plan.slot(Pack::VALIDITY_SLOT)?.is_none()); + assert_eq!(child_of(&plan, Pack::FIELDS_OFFSET)?.dtype(), &field_dtype); + assert_eq!( + child_of(&plan, Pack::FIELDS_OFFSET + 1)?.dtype(), + &field_dtype + ); let nullable = StructLayout::new( 3, @@ -307,16 +311,43 @@ fn struct_plan_appends_validity_when_nullable() -> VortexResult<()> { ) .into_layout(); let nullable_plan = make_plan(nullable)?; - assert_eq!(nullable_plan.child_count(), 3); + assert_eq!(nullable_plan.nslots(), 3); assert_eq!( - child_of(&nullable_plan, 2)?.dtype(), + child_of(&nullable_plan, Pack::VALIDITY_SLOT)?.dtype(), &DType::Bool(Nullability::NonNullable) ); Ok(()) } #[test] -fn with_children_rejects_mismatched_arity() -> VortexResult<()> { +fn pack_validity_slot_must_match_nullability() -> VortexResult<()> { + let field_dtype = primitive(PType::I32, Nullability::NonNullable); + let fields = StructFields::from_iter([("a", field_dtype.clone())]); + let plan = make_plan( + StructLayout::new( + 3, + DType::Struct(fields, Nullability::NonNullable), + vec![flat(3, field_dtype, 0)], + ) + .into_layout(), + )?; + let validity = make_plan(flat(3, DType::Bool(Nullability::NonNullable), 1))?; + + let error = plan + .with_slots(vec![validity, child_of(&plan, Pack::FIELDS_OFFSET)?]) + .err() + .ok_or_else(|| vortex_err!("validity on a non-nullable Pack unexpectedly succeeded"))?; + assert!( + error + .to_string() + .contains("Non-nullable Pack has a validity child"), + "unexpected error: {error}" + ); + Ok(()) +} + +#[test] +fn with_slots_rejects_mismatched_arity() -> VortexResult<()> { let dtype = primitive(PType::I32, Nullability::NonNullable); let layout = ChunkedLayout::new( 3, @@ -327,20 +358,20 @@ fn with_children_rejects_mismatched_arity() -> VortexResult<()> { let plan = make_plan(layout)?; let error = plan - .with_children(vec![child_of(&plan, 0)?]) + .with_slots(vec![child_of(&plan, 0)?]) .err() .ok_or_else(|| vortex_err!("mismatched arity unexpectedly succeeded"))?; assert!( error .to_string() - .contains("Concat expects 2 children but got 1"), + .contains("Concat expects 2 slots but got 1"), "unexpected error: {error}" ); Ok(()) } #[test] -fn with_children_replaces_children_in_order() -> VortexResult<()> { +fn with_slots_replaces_children_in_order() -> VortexResult<()> { let dtype = primitive(PType::I32, Nullability::NonNullable); let layout = ChunkedLayout::new( 3, @@ -350,7 +381,7 @@ fn with_children_replaces_children_in_order() -> VortexResult<()> { .into_layout(); let plan = make_plan(layout)?; - let swapped = plan.with_children(vec![child_of(&plan, 1)?, child_of(&plan, 0)?])?; + let swapped = plan.with_slots(vec![child_of(&plan, 1)?, child_of(&plan, 0)?])?; assert_eq!(child_of(&swapped, 0)?.row_count(), 1); assert_eq!(child_of(&swapped, 1)?.row_count(), 2); assert_eq!(swapped.as_::().row_offsets(), &[0, 1]); @@ -399,7 +430,7 @@ fn optimize_rewrites_nested_children() -> VortexResult<()> { // any chunked-specific rule. let chunk = child_of(&chunked, 0)?; let identity: PlanRef = EvalPlan::try_new(root().bind(chunk.dtype())?, chunk)?.into_plan(); - let wrapped = chunked.with_children(vec![identity, child_of(&chunked, 1)?])?; + let wrapped = chunked.with_slots(vec![identity, child_of(&chunked, 1)?])?; let optimized = optimize(wrapped, &SESSION)?; assert!(optimized.is::()); @@ -476,12 +507,12 @@ fn plan_display_matches_array_tree_display_shape() -> VortexResult<()> { let nullable = make_plan(nullable_layout)?; insta::assert_snapshot!(nullable.tree_display_builder(), @r" root: + validity: + child: a: child: b: child: - validity: - child: "); Ok(()) } @@ -659,7 +690,8 @@ fn empty_projection_prunes_row_idx_child_fields() -> VortexResult<()> { .ok_or_else(|| vortex_err!("empty projection did not prune the RowIdx child"))?; assert_eq!(empty_struct.nfields(), 0); - assert_eq!(empty_struct.children().len(), 0); + assert_eq!(empty_struct.nslots(), Pack::FIELDS_OFFSET); + assert_eq!(empty_struct.children().count(), 0); Ok(()) } @@ -1313,7 +1345,8 @@ fn stats_expression_rewrites_to_zoned_pruning_plan() -> VortexResult<()> { assert!(zoned.is_pruning()); assert_eq!(zoned.pruning_expression(), Some(&falsifier)); assert!(zoned.data_plan()?.is_none()); - assert_eq!(zoned.children().len(), 1); + assert_eq!(zoned.nslots(), 2); + assert_eq!(zoned.children().count(), 1); let mixed_expression = bound_and(falsifier, gt(root(), lit(0_i32)).bind(&dtype)?); let mixed = optimize( @@ -1760,7 +1793,10 @@ fn zoned_pruning_plan_with_new_zones_proves_them_again() -> VortexResult<()> { ); let new_zones = make_plan(flat(2, zones_dtype, 2))?; - let rebuilt = pruning.clone().into_plan().with_children(vec![new_zones])?; + let rebuilt = pruning + .clone() + .into_plan() + .with_slots(vec![None, Some(LazyPlanRef::ready(new_zones))])?; let rebuilt_proof = rebuilt .as_opt::() .and_then(|zoned| zoned.pruned_zones()) diff --git a/vortex-layout/src/plan/physical/typed.rs b/vortex-layout/src/plan/physical/typed.rs index c783dc8137b..ec351cd676f 100644 --- a/vortex-layout/src/plan/physical/typed.rs +++ b/vortex-layout/src/plan/physical/typed.rs @@ -19,11 +19,10 @@ use vortex_array::dtype::DType; use vortex_error::VortexExpect; use vortex_error::VortexResult; use vortex_error::vortex_ensure; -use vortex_error::vortex_err; use vortex_mask::Mask; -use crate::plan::physical::PlanChildren; use crate::plan::physical::PlanId; +use crate::plan::physical::PlanSlots; use crate::plan::physical::PlanVTable; use crate::plan::physical::display::PlanTreeDisplay; use crate::plan::physical::pipeline::Chain; @@ -32,13 +31,13 @@ use crate::plan::physical::pipeline::Compiler; /// The combined allocation behind [`PlanRef`]. /// /// Common plan state is stored before the unsized `data` tail, so reading the operator ID, dtype, -/// row count, or children does not dispatch through the operator vtable. Only `PlanData` is +/// row count, or slots does not dispatch through the operator vtable. Only `PlanData` is /// erased to [`DynPlan`]. struct PlanInner { id: PlanId, dtype: DType, row_count: u64, - children: PlanChildren, + slots: PlanSlots, data: D, // must be last for unsized coercion } @@ -76,35 +75,41 @@ impl PlanRef { self.0.row_count } - /// Returns the common child container without initializing any child. - pub fn children(&self) -> &PlanChildren { - &self.0.children + /// Returns the child slots without building any child. + pub fn slots(&self) -> &PlanSlots { + &self.0.slots } - /// Returns the number of children without initializing any child. - pub fn child_count(&self) -> usize { - self.0.children.len() + /// Returns the number of slots, present or absent, without building any child. + pub fn nslots(&self) -> usize { + self.0.slots.len() } - /// Returns the child at `index`, initializing it on first access. - pub fn child(&self, index: usize) -> VortexResult> { - self.0.children.get(index) + /// Returns the plan in `slot`, building it on first access, or `None` when the slot is + /// absent. Errors when `slot` is out of bounds. + pub fn slot(&self, slot: usize) -> VortexResult> { + Ok(self.0.slots.get(slot)?.cloned()) } - /// Returns the child at `index`, or an error when the index is out of bounds. - pub fn child_required(&self, index: usize) -> VortexResult { - self.child(index)? - .ok_or_else(|| vortex_err!("Missing plan child {index}")) + /// Returns the plan in `slot`, or an error when the slot is absent or out of bounds. + pub fn slot_required(&self, slot: usize) -> VortexResult { + self.0.slots.required(slot).cloned() } - /// Rebuilds this plan with `children` stored outside its erased operator data. - pub fn with_children(&self, children: impl Into) -> VortexResult { - self.dyn_plan().dyn_with_children(self, children.into()) + /// The plans of the present slots in slot order, building each as it is visited. See + /// [`PlanSlots::children`]. + pub fn children(&self) -> impl Iterator> + '_ { + self.0.slots.children() } - /// Returns the display name of the child at `index`. - pub fn child_name(&self, index: usize) -> Cow<'_, str> { - self.dyn_plan().dyn_child_name(self, index) + /// Rebuilds this plan with `slots` stored outside its erased operator data. + pub fn with_slots(&self, slots: impl Into) -> VortexResult { + self.dyn_plan().dyn_with_slots(self, slots.into()) + } + + /// Returns the display name of `slot`. + pub fn slot_name(&self, slot: usize) -> Cow<'_, str> { + self.dyn_plan().dyn_slot_name(self, slot) } /// Serializes operator-specific metadata, or `None` when the operator is not serializable. @@ -185,7 +190,7 @@ impl Debug for PlanRef { .field("id", &self.0.id) .field("dtype", &self.0.dtype) .field("row_count", &self.0.row_count) - .field("children", &self.0.children) + .field("slots", &self.0.slots) .field("data", &&self.0.data) .finish() } @@ -199,8 +204,8 @@ pub struct PlanParts { pub dtype: DType, /// Number of rows in the operator's row domain. pub row_count: u64, - /// Child operators, in stable logical order. - pub children: PlanChildren, + /// Child slots, at the positions the operator defines. + pub slots: PlanSlots, /// Operator-specific, non-child data. pub data: V::PlanData, } @@ -253,7 +258,7 @@ impl Plan { id: parts.vtable.id(), dtype: parts.dtype, row_count: parts.row_count, - children: parts.children, + slots: parts.slots, data: PlanData { vtable: parts.vtable, data: parts.data, @@ -293,19 +298,30 @@ impl Plan { self.inner.row_count() } - /// Returns the common child container without initializing any child. - pub fn children(&self) -> &PlanChildren { - self.inner.children() + /// Returns the child slots without building any child. + pub fn slots(&self) -> &PlanSlots { + self.inner.slots() + } + + /// Returns the number of slots, present or absent, without building any child. + pub fn nslots(&self) -> usize { + self.inner.nslots() + } + + /// Returns the plan in `slot`, building it on first access, or `None` when the slot is + /// absent. + pub fn slot(&self, slot: usize) -> VortexResult> { + self.inner.slot(slot) } - /// Returns a child, initializing it on first access. - pub fn child(&self, index: usize) -> VortexResult> { - self.inner.child(index) + /// Returns the plan in `slot`, or an error when the slot is absent or out of bounds. + pub fn slot_required(&self, slot: usize) -> VortexResult { + self.inner.slot_required(slot) } - /// Returns the child at `index`, or an error when the index is out of bounds. - pub fn child_required(&self, index: usize) -> VortexResult { - self.inner.child_required(index) + /// The plans of the present slots in slot order. See [`PlanSlots::children`]. + pub fn children(&self) -> impl Iterator> + '_ { + self.inner.children() } /// Erases this typed plan into a shared reference. @@ -379,11 +395,11 @@ pub trait DynPlan: 'static + Send + Sync + Debug { /// Formats operator-specific fields. fn dyn_fmt(&self, plan: &PlanRef, formatter: &mut Formatter<'_>) -> fmt::Result; - /// Clones operator data, runs its child-replacement callback, and rebuilds the common node. - fn dyn_with_children(&self, plan: &PlanRef, children: PlanChildren) -> VortexResult; + /// Clones operator data, runs its slot-replacement callback, and rebuilds the common node. + fn dyn_with_slots(&self, plan: &PlanRef, slots: PlanSlots) -> VortexResult; - /// Returns the display name of the child at `index`. - fn dyn_child_name<'a>(&'a self, plan: &'a PlanRef, index: usize) -> Cow<'a, str>; + /// Returns the display name of `slot`. + fn dyn_slot_name<'a>(&'a self, plan: &'a PlanRef, slot: usize) -> Cow<'a, str>; /// Serializes operator-specific metadata, or `None` when the operator is not serializable. fn dyn_metadata(&self, plan: &PlanRef) -> Option>; @@ -407,21 +423,21 @@ impl DynPlan for PlanData { ::fmt(plan.as_::(), formatter) } - fn dyn_with_children(&self, plan: &PlanRef, children: PlanChildren) -> VortexResult { + fn dyn_with_slots(&self, plan: &PlanRef, slots: PlanSlots) -> VortexResult { let mut data = self.data.clone(); - V::with_children(plan.as_::(), &children, &mut data)?; + V::with_slots(plan.as_::(), &slots, &mut data)?; Ok(PlanParts { vtable: self.vtable.clone(), dtype: plan.dtype().clone(), row_count: plan.row_count(), - children, + slots, data, } .into_plan()) } - fn dyn_child_name<'a>(&'a self, plan: &'a PlanRef, index: usize) -> Cow<'a, str> { - V::child_name(plan.as_::(), index) + fn dyn_slot_name<'a>(&'a self, plan: &'a PlanRef, slot: usize) -> Cow<'a, str> { + V::slot_name(plan.as_::(), slot) } fn dyn_metadata(&self, plan: &PlanRef) -> Option> { diff --git a/vortex-layout/src/plan/physical/vtable.rs b/vortex-layout/src/plan/physical/vtable.rs index 75f3e4139c8..347c72c1170 100644 --- a/vortex-layout/src/plan/physical/vtable.rs +++ b/vortex-layout/src/plan/physical/vtable.rs @@ -13,7 +13,7 @@ use vortex_error::vortex_bail; use vortex_mask::Mask; use vortex_session::registry::Id; -use crate::plan::physical::PlanChildren; +use crate::plan::physical::PlanSlots; use crate::plan::physical::pipeline::Chain; use crate::plan::physical::pipeline::Compiler; use crate::plan::physical::typed::Plan; @@ -23,17 +23,20 @@ pub type PlanId = Id; /// Operator-specific behavior for a typed [`Plan`]. /// -/// Common fields — dtype, row count, and children — are stored outside the erased operator data. -/// Implementations own only their operator-specific data, its metadata codec, and a callback for -/// refreshing cached data after generic child replacement. +/// Common fields — dtype, row count, and child slots — are stored outside the erased operator +/// data. Implementations own only their operator-specific data, its metadata codec, and a callback +/// for refreshing cached data after generic slot replacement. +/// +/// Each operator fixes what every slot position holds. An absent child is a `None` slot, so the +/// positions of the others never depend on which children are present. /// /// Operators describe physical work over a row domain. Their identity and operator-specific data -/// do not depend on the source layout kind. The common lazy-child storage may nevertheless own a -/// hidden source-layout reference used for on-demand lowering. +/// do not depend on the source layout kind. A lazy slot may nevertheless own a hidden +/// source-layout reference used for on-demand lowering. pub trait PlanVTable: 'static + Clone + Sized + Send + Sync + Debug { /// Operator-specific data, excluding children. /// - /// Children belong in [`PlanParts::children`](crate::plan::physical::PlanParts::children) so that + /// Children belong in [`PlanParts::slots`](crate::plan::physical::PlanParts::slots) so that /// traversal and rewriting can discover them generically. type PlanData: 'static + Send + Sync + Clone + Debug; @@ -54,24 +57,24 @@ pub trait PlanVTable: 'static + Clone + Sized + Send + Sync + Debug { /// Returns `None` when the operator holds state that cannot be serialized. fn metadata(plan: &Plan) -> Option; - /// Refreshes cloned operator data after the common child container is replaced. + /// Refreshes cloned operator data after the slots are replaced. /// - /// The plan layer clones [`PlanData`](Self::PlanData), replaces the children externally, and - /// invokes this callback. Implementations validate the new children and update any derived + /// The plan layer clones [`PlanData`](Self::PlanData), replaces the slots externally, and + /// invokes this callback. Implementations validate the new slots and update any derived /// values in `data`; they do not rebuild the plan itself. - fn with_children( + fn with_slots( plan: &Plan, - children: &PlanChildren, + slots: &PlanSlots, data: &mut Self::PlanData, ) -> VortexResult<()> { - let _ = (plan, children, data); + let _ = (plan, slots, data); Ok(()) } - /// Returns the display name of the child at `index`. - fn child_name(plan: &Plan, index: usize) -> Cow<'_, str> { + /// Returns the display name of `slot`. + fn slot_name(plan: &Plan, slot: usize) -> Cow<'_, str> { let _ = plan; - Cow::Owned(format!("child[{index}]")) + Cow::Owned(format!("slot[{slot}]")) } /// Compiles this plan over `rows` of its row domain, restricted to `mask`, into a chain: @@ -81,6 +84,10 @@ pub trait PlanVTable: 'static + Clone + Sized + Send + Sync + Debug { /// /// A plan compiles its children through `compiler` and either adds stages to a child's /// chain or joins several children's chains with a source of its own. Compiling does no IO. + /// + /// Pipelines have no absent inputs: a plan that joins its children compiles + /// [`Plan::children`], the present slots in slot order, so inlet `i` of its source reads the + /// `i`-th present slot. fn compile( plan: &Plan, rows: Range, diff --git a/vortex-layout/src/scan/execution/filter.rs b/vortex-layout/src/scan/execution/filter.rs index d3d7b640892..4278d31001d 100644 --- a/vortex-layout/src/scan/execution/filter.rs +++ b/vortex-layout/src/scan/execution/filter.rs @@ -112,8 +112,8 @@ fn metadata_only(plan: &PlanRef) -> VortexResult { if plan.is::() { return Ok(false); } - for child in plan.children().iter() { - if !metadata_only(&child?)? { + for child in plan.children() { + if !metadata_only(child?)? { return Ok(false); } } From a3238df7df8ea0329d9b1ffcd17a63ec198e754b Mon Sep 17 00:00:00 2001 From: ump45nose <52391318+ump45nose@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:22:07 +0800 Subject: [PATCH 03/41] Return an error for decimal scalars outside the precision (#10767) - Tracking Issue: #10749 `DecimalArray::scalar_at` and the decimal `min_max` aggregate panicked when a stored value did not fit the precision of the dtype, because both built their results with `Scalar::decimal`, which calls `vortex_expect` on the precision check. Since `DecimalArray` construction doesn't check its values and file reads trust the stored precision, a file holding such a value could make a read panic. With this change both calls return an error instead. **Still open:** the panic is still reachable through cached stats. `min_max` returns a cached `Stat::Min`/`Stat::Max` before it reaches the kernel, and decimal stats decoded from a file are not checked against the precision, so an out-of-range stored stat still panics in `stat_scalar`. That needs a check at the read boundary and is tracked in #10807. The other `Scalar::decimal` callers on compute paths (`sum`, `mean`, `cast`, `fill_null`, `between`, the DecimalByteParts kernels) are also left for follow-ups. --------- Signed-off-by: Vincent <1729065730@qq.com> Signed-off-by: Connor Tsui --- .../src/decimal_byte_parts/array.rs | 3 +- fuzz/src/array/scalar_at.rs | 4 +- .../src/aggregate_fn/fns/min_max/decimal.rs | 28 +++---- .../src/aggregate_fn/fns/min_max/mod.rs | 48 +++++++++++ vortex-array/src/arrays/decimal/array.rs | 23 +++--- .../src/arrays/decimal/vtable/operations.rs | 51 +++++++++++- vortex-array/src/scalar/constructor.rs | 21 ++++- vortex-array/src/stats/flatbuffers.rs | 81 ++++++++++++++++--- vortex-python/src/scalar/factory.rs | 7 +- 9 files changed, 219 insertions(+), 47 deletions(-) diff --git a/encodings/decimal-byte-parts/src/decimal_byte_parts/array.rs b/encodings/decimal-byte-parts/src/decimal_byte_parts/array.rs index f0b03720e00..796789d5799 100644 --- a/encodings/decimal-byte-parts/src/decimal_byte_parts/array.rs +++ b/encodings/decimal-byte-parts/src/decimal_byte_parts/array.rs @@ -26,7 +26,6 @@ use vortex_array::dtype::DecimalDType; use vortex_array::require_child; use vortex_array::scalar::DecimalValue; use vortex_array::scalar::Scalar; -use vortex_array::scalar::ScalarValue; use vortex_array::serde::ArrayChildren; use vortex_array::vtable::OperationsVTable; use vortex_array::vtable::VTable; @@ -397,7 +396,7 @@ impl OperationsVTable for DecimalByteParts { ), }; - Scalar::try_new(array.dtype().clone(), Some(ScalarValue::Decimal(value))) + Scalar::try_decimal(value, array.decimal_dtype(), array.dtype().nullability()) } } diff --git a/fuzz/src/array/scalar_at.rs b/fuzz/src/array/scalar_at.rs index c978aacbc0d..28b85fcf810 100644 --- a/fuzz/src/array/scalar_at.rs +++ b/fuzz/src/array/scalar_at.rs @@ -48,11 +48,11 @@ pub fn scalar_at_canonical_array( } Canonical::Decimal(array) => { match_each_decimal_value_type!(array.values_type(), |D| { - Scalar::decimal( + Scalar::try_decimal( DecimalValue::from(array.buffer::()[index]), array.decimal_dtype(), array.dtype().nullability(), - ) + )? }) } Canonical::VarBinView(array) => varbin_scalar(array.bytes_at(index), array.dtype()), diff --git a/vortex-array/src/aggregate_fn/fns/min_max/decimal.rs b/vortex-array/src/aggregate_fn/fns/min_max/decimal.rs index f7c0a633d69..c09fb2f04f6 100644 --- a/vortex-array/src/aggregate_fn/fns/min_max/decimal.rs +++ b/vortex-array/src/aggregate_fn/fns/min_max/decimal.rs @@ -46,21 +46,21 @@ where let decimal_dtype = array.decimal_dtype(); - Ok( - valid_range(array.buffer::().as_slice(), &validity).map(|(min, max)| { - let min_scalar = Scalar::decimal(min.into(), decimal_dtype, NonNullable); - let max_scalar = if min == max { - min_scalar.clone() - } else { - Scalar::decimal(max.into(), decimal_dtype, NonNullable) - }; + let Some((min, max)) = valid_range(array.buffer::().as_slice(), &validity) else { + return Ok(None); + }; + + let min_scalar = Scalar::try_decimal(min.into(), decimal_dtype, NonNullable)?; + let max_scalar = if min == max { + min_scalar.clone() + } else { + Scalar::try_decimal(max.into(), decimal_dtype, NonNullable)? + }; - MinMaxResult { - min: min_scalar, - max: max_scalar, - } - }), - ) + Ok(Some(MinMaxResult { + min: min_scalar, + max: max_scalar, + })) } /// Returns the smallest and the largest valid value, or `None` when no value is valid. diff --git a/vortex-array/src/aggregate_fn/fns/min_max/mod.rs b/vortex-array/src/aggregate_fn/fns/min_max/mod.rs index 21b0c50f94d..303ca9c5d07 100644 --- a/vortex-array/src/aggregate_fn/fns/min_max/mod.rs +++ b/vortex-array/src/aggregate_fn/fns/min_max/mod.rs @@ -1093,6 +1093,54 @@ mod tests { }) } + #[rstest] + #[case::one_element(buffer![100i8])] + #[case::min_max(buffer![1i8, 100i8])] + #[case::negative_min(buffer![-100i8, 1i8])] + fn test_decimal_value_outside_precision_errors(#[case] values: Buffer) -> VortexResult<()> { + // 100 and -100 do not fit in decimal(2, 0), and `DecimalArray::new` does not check the + // values. + let decimal = DecimalArray::new(values, DecimalDType::new(2, 0), Validity::NonNullable); + let mut ctx = SESSION.create_execution_ctx(); + let err = min_max( + &decimal.into_array(), + &mut ctx, + NumericalAggregateOpts::default(), + ) + .unwrap_err(); + assert!( + err.to_string().contains("does not fit in precision"), + "unexpected error: {err}" + ); + Ok(()) + } + + #[test] + fn test_decimal_value_outside_precision_in_null_slot() -> VortexResult<()> { + // The out-of-range value sits in a null slot, so it must be ignored. + let decimal = DecimalArray::new( + buffer![5i8, 100i8], + DecimalDType::new(2, 0), + Validity::from_iter([true, false]), + ); + let mut ctx = SESSION.create_execution_ctx(); + let result = min_max( + &decimal.into_array(), + &mut ctx, + NumericalAggregateOpts::default(), + )? + .vortex_expect("should have result"); + + let expected = Scalar::decimal( + DecimalValue::I8(5), + DecimalDType::new(2, 0), + Nullability::NonNullable, + ); + assert_eq!(result.min, expected); + assert_eq!(result.max, expected); + Ok(()) + } + #[test] fn list_and_fixed_size_list_return_dtype() { let element_dtype = DType::Primitive(PType::I32, Nullability::Nullable); diff --git a/vortex-array/src/arrays/decimal/array.rs b/vortex-array/src/arrays/decimal/array.rs index 61aaf898515..91521aab93e 100644 --- a/vortex-array/src/arrays/decimal/array.rs +++ b/vortex-array/src/arrays/decimal/array.rs @@ -251,15 +251,15 @@ impl DecimalData { /// /// * `buffer` is a typed buffer containing the decimal values. /// * `decimal_dtype` specifies the decimal precision and scale. - /// * `validity` holds the null values. /// - /// # Safety + /// Values are not checked against the precision of `decimal_dtype`, either here, in + /// [`DecimalArray::try_new`], or when an array is read from a file. Code that turns stored values + /// into scalars must use a fallible path such as + /// [`Scalar::try_decimal`](crate::scalar::Scalar::try_decimal). /// - /// The caller must ensure all of the following invariants are satisfied: + /// # Safety /// - /// - All non-null values in `buffer` must be representable within the specified precision. - /// - For example, with precision=5 and scale=2, all values must be in range [-999.99, 999.99]. - /// - If `validity` is [`Validity::Array`], its length must exactly equal `buffer.len()`. + /// `buffer` is a typed [`Buffer`], so there are no further requirements on the caller. pub unsafe fn new_unchecked( buffer: Buffer, decimal_dtype: DecimalDType, @@ -276,13 +276,15 @@ impl DecimalData { /// Create a new array with decimal values backed by the given buffer handle. /// + /// As with [`DecimalArray::new_unchecked`], values are not checked against the precision of + /// `decimal_dtype`. + /// /// # Safety /// - /// The caller must ensure all of the following invariants are satisfied: + /// - The length of `values` must be a multiple of the byte width of `values_type`. + /// - `values` must be aligned to the native type of `values_type`. /// - /// - All non-null values in `values` must be representable within the specified precision. - /// - For example, with precision=5 and scale=2, all values must be in range [-999.99, 999.99]. - /// - If `validity` is [`Validity::Array`], its length must exactly equal `buffer.len()`. + /// Breaking either rule does not cause undefined behavior, but [`DecimalData::buffer`] panics. pub unsafe fn new_unchecked_handle( values: BufferHandle, values_type: DecimalType, @@ -325,7 +327,6 @@ impl DecimalData { /// The caller must ensure: /// - The `byte_buffer` contains valid data for the specified `values_type` /// - The buffer length is compatible with the `values_type` (i.e., divisible by the type size) - /// - All non-null values are representable within the specified precision /// - If `validity` is [`Validity::Array`], its length must equal the number of elements pub unsafe fn new_unchecked_from_byte_buffer( byte_buffer: ByteBuffer, diff --git a/vortex-array/src/arrays/decimal/vtable/operations.rs b/vortex-array/src/arrays/decimal/vtable/operations.rs index 09d1d31a29c..c43ff65c7a6 100644 --- a/vortex-array/src/arrays/decimal/vtable/operations.rs +++ b/vortex-array/src/arrays/decimal/vtable/operations.rs @@ -19,13 +19,14 @@ impl OperationsVTable for Decimal { index: usize, _ctx: &mut ExecutionCtx, ) -> VortexResult { - Ok(match_each_decimal_value_type!(array.values_type(), |D| { - Scalar::decimal( + // Stored values can fall outside the precision, see `DecimalData::new_unchecked`. + match_each_decimal_value_type!(array.values_type(), |D| { + Scalar::try_decimal( DecimalValue::from(array.buffer::()[index]), array.decimal_dtype(), array.dtype().nullability(), ) - })) + }) } } @@ -33,7 +34,9 @@ impl OperationsVTable for Decimal { mod tests { use std::sync::LazyLock; + use rstest::rstest; use vortex_buffer::buffer; + use vortex_error::VortexResult; use vortex_session::VortexSession; use crate::IntoArray; @@ -102,4 +105,46 @@ mod tests { ) ); } + + #[rstest] + #[case::positive(100i8)] + #[case::negative(-100i8)] + fn test_scalar_at_value_outside_precision_errors(#[case] value: i8) -> VortexResult<()> { + // `DecimalArray::new` does not check that the values fit the precision. + let array = DecimalArray::new( + buffer![value], + DecimalDType::new(2, 0), + Validity::NonNullable, + ); + + let err = array + .execute_scalar(0, &mut SESSION.create_execution_ctx()) + .unwrap_err(); + assert!( + err.to_string().contains("does not fit in precision"), + "unexpected error: {err}" + ); + Ok(()) + } + + #[test] + fn test_scalar_at_ignores_value_outside_precision_in_null_slot() -> VortexResult<()> { + let array = DecimalArray::new( + buffer![5i8, 100i8], + DecimalDType::new(2, 0), + Validity::from_iter([true, false]), + ); + let mut ctx = SESSION.create_execution_ctx(); + + assert_eq!( + array.execute_scalar(0, &mut ctx)?, + Scalar::decimal( + DecimalValue::I8(5), + DecimalDType::new(2, 0), + Nullability::Nullable + ) + ); + assert!(array.execute_scalar(1, &mut ctx)?.is_null()); + Ok(()) + } } diff --git a/vortex-array/src/scalar/constructor.rs b/vortex-array/src/scalar/constructor.rs index 1e7d84ed40b..622b0db32b3 100644 --- a/vortex-array/src/scalar/constructor.rs +++ b/vortex-array/src/scalar/constructor.rs @@ -65,16 +65,35 @@ impl Scalar { } /// Creates a new decimal scalar with the given value, precision, scale, and nullability. + /// + /// # Panics + /// + /// Panics if `value` does not fit in the precision of `decimal_type`. Use + /// [`Scalar::try_decimal`] when the value comes from data that has not been validated. pub fn decimal( value: DecimalValue, decimal_type: DecimalDType, nullability: Nullability, ) -> Self { + Self::try_decimal(value, decimal_type, nullability) + .vortex_expect("decimal value must fit the precision of `decimal_type`") + } + + /// Attempts to create a new decimal scalar with the given value, precision, scale, and + /// nullability. + /// + /// # Errors + /// + /// Returns an error if `value` does not fit in the precision of `decimal_type`. + pub fn try_decimal( + value: DecimalValue, + decimal_type: DecimalDType, + nullability: Nullability, + ) -> VortexResult { Self::try_new( DType::Decimal(decimal_type, nullability), Some(ScalarValue::Decimal(value)), ) - .vortex_expect("unable to construct a decimal `Scalar`") } /// Creates a new UTF-8 scalar from a string-like value. diff --git a/vortex-array/src/stats/flatbuffers.rs b/vortex-array/src/stats/flatbuffers.rs index 4d78c5bfe85..10c1b744ef4 100644 --- a/vortex-array/src/stats/flatbuffers.rs +++ b/vortex-array/src/stats/flatbuffers.rs @@ -16,6 +16,7 @@ use crate::expr::stats::Stat; use crate::flatbuffers::WriteFlatBuffer; use crate::flatbuffers::array as fba; use crate::scalar::PValue; +use crate::scalar::Scalar; use crate::scalar::ScalarValue; use crate::stats::StatsSet; use crate::stats::StatsSetRef; @@ -154,9 +155,8 @@ impl StatsSet { if let Some(max) = fb.max() && let Some(stat_dtype) = stat.dtype(array_dtype) { - let value = - ScalarValue::from_proto_bytes(max.bytes(), &stat_dtype, session)?; - let Some(value) = value else { + let Some(value) = decode_stat_value(max.bytes(), &stat_dtype, session)? + else { continue; }; @@ -176,9 +176,8 @@ impl StatsSet { if let Some(min) = fb.min() && let Some(stat_dtype) = stat.dtype(array_dtype) { - let value = - ScalarValue::from_proto_bytes(min.bytes(), &stat_dtype, session)?; - let Some(value) = value else { + let Some(value) = decode_stat_value(min.bytes(), &stat_dtype, session)? + else { continue; }; @@ -211,9 +210,8 @@ impl StatsSet { if let Some(sum) = fb.sum() && let Some(stat_dtype) = stat.dtype(array_dtype) { - let value = - ScalarValue::from_proto_bytes(sum.bytes(), &stat_dtype, session)?; - let Some(value) = value else { + let Some(value) = decode_stat_value(sum.bytes(), &stat_dtype, session)? + else { continue; }; @@ -235,21 +233,44 @@ impl StatsSet { } } +/// Decodes a stored statistic value and checks that it is a legal value of `stat_dtype`. +/// +/// Returns `None` for a stored null. Vortex never writes an illegal statistic, for example a +/// decimal outside its precision, so an illegal one means the file is corrupt. The check runs at +/// decode because [`StatsProvider::get`] has no error path, so it panics on an illegal value. +/// +/// [`StatsProvider::get`]: crate::expr::stats::StatsProvider::get +fn decode_stat_value( + bytes: &[u8], + stat_dtype: &DType, + session: &VortexSession, +) -> VortexResult> { + let Some(value) = ScalarValue::from_proto_bytes(bytes, stat_dtype, session)? else { + return Ok(None); + }; + Scalar::validate(stat_dtype, Some(&value))?; + + Ok(Some(value)) +} + #[cfg(test)] mod tests { use flatbuffers::FlatBufferBuilder; use flatbuffers::root; + use rstest::rstest; use vortex_error::VortexResult; use vortex_error::vortex_err; use vortex_session::VortexSession; use crate::dtype::DType; + use crate::dtype::DecimalDType; use crate::dtype::Nullability; use crate::dtype::PType; use crate::expr::stats::Precision; use crate::expr::stats::Stat; use crate::flatbuffers::WriteFlatBuffer; use crate::flatbuffers::array as fba; + use crate::scalar::DecimalValue; use crate::scalar::PValue; use crate::scalar::ScalarValue; use crate::stats::StatsSet; @@ -281,6 +302,19 @@ mod tests { .to_bits()) } + /// Writes one exact decimal statistic and reads it back for a `decimal(2, 0)` column. + fn roundtrip_decimal_stat(stat: Stat, value: DecimalValue) -> VortexResult { + let stats = StatsSet::of(stat, Precision::exact(ScalarValue::Decimal(value))); + + let mut fbb = FlatBufferBuilder::new(); + let offset = stats.write_flatbuffer(&mut fbb)?; + fbb.finish_minimal(offset); + let fb = root::(fbb.finished_data())?; + + let dtype = DType::Decimal(DecimalDType::new(2, 0), Nullability::NonNullable); + StatsSet::from_flatbuffer(&fb, &dtype, &VortexSession::empty()) + } + #[test] fn nan_sum_is_written_canonical() -> VortexResult<()> { // x86_64 gives this NaN for `inf + -inf`. @@ -301,4 +335,33 @@ mod tests { Ok(()) } + + // The sum of a `decimal(2, 0)` column has the dtype `decimal(12, 0)`. + #[rstest] + #[case::max(Stat::Max, DecimalValue::I8(100))] + #[case::min(Stat::Min, DecimalValue::I8(-100))] + #[case::sum(Stat::Sum, DecimalValue::I64(1_000_000_000_000))] + fn decimal_stat_outside_precision_fails_to_read( + #[case] stat: Stat, + #[case] value: DecimalValue, + ) -> VortexResult<()> { + let err = roundtrip_decimal_stat(stat, value).unwrap_err(); + assert!( + err.to_string().contains("does not fit in precision"), + "unexpected error: {err}" + ); + + Ok(()) + } + + #[test] + fn decimal_stat_inside_precision_reads() -> VortexResult<()> { + let read = roundtrip_decimal_stat(Stat::Max, DecimalValue::I8(99))?; + assert_eq!( + read.get(Stat::Max), + Precision::exact(ScalarValue::Decimal(DecimalValue::I8(99))) + ); + + Ok(()) + } } diff --git a/vortex-python/src/scalar/factory.rs b/vortex-python/src/scalar/factory.rs index 4cb1649f1ac..a3fc0675a8b 100644 --- a/vortex-python/src/scalar/factory.rs +++ b/vortex-python/src/scalar/factory.rs @@ -155,11 +155,8 @@ fn scalar_helper_inner(value: &Bound<'_, PyAny>, dtype: Option<&DType>) -> PyRes "Value can't be represented as decimal", )); }; - return Ok(Scalar::decimal( - value, - *decimal_dtype, - Nullability::NonNullable, - )); + return Scalar::try_decimal(value, *decimal_dtype, Nullability::NonNullable) + .map_err(|e| PyValueError::new_err(e.to_string())); } if let Ok(integer) = value.cast::() { From 93ab9aa4fdfb647afaf69db0b92e622aaa702635 Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Wed, 4 Nov 2026 11:48:22 +0000 Subject: [PATCH 04/41] Remove x8g from benchmark runner instance families (#11199) Remove `x8g.8xlarge` from the list of instance families that the dedicated benchmark runners can use. Signed-off-by: Robert Kruszewski --- .github/workflows/develop-bench.yml | 4 +--- .github/workflows/pr-bench-runner.yml | 4 +--- .github/workflows/sql-bench-matrix.yml | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/develop-bench.yml b/.github/workflows/develop-bench.yml index be17f19bad0..e2b0a26f4e6 100644 --- a/.github/workflows/develop-bench.yml +++ b/.github/workflows/develop-bench.yml @@ -14,11 +14,9 @@ permissions: jobs: build: timeout-minutes: 60 - # GitHub drops runner labels longer than 255 characters, and the job then waits for a runner - # forever. With the long `family` list and the longest benchmark id, this label is close. runs-on: >- ${{ github.repository == 'vortex-data/vortex' - && format('runs-on={0}/region=us-east-1/runner=bench-dedicated/family=c8gd.8xlarge+c8g.8xlarge+c8gn.8xlarge+m8gd.8xlarge+m8g.8xlarge+r8gd.8xlarge+r8g.8xlarge+x8g.8xlarge+i8g.8xlarge/image=ubuntu26-full-arm64-pre-v2/extras=s3-cache/tag={1}', github.run_id, matrix.benchmark.id) + && format('runs-on={0}/region=us-east-1/runner=arm64-bench/extras=s3-cache/tag={1}', github.run_id, matrix.benchmark.id) || 'ubuntu-latest' }} strategy: fail-fast: false diff --git a/.github/workflows/pr-bench-runner.yml b/.github/workflows/pr-bench-runner.yml index 4e7f73b2af2..37298385141 100644 --- a/.github/workflows/pr-bench-runner.yml +++ b/.github/workflows/pr-bench-runner.yml @@ -44,11 +44,9 @@ permissions: jobs: build: timeout-minutes: 60 - # GitHub drops runner labels longer than 255 characters, and the job then waits for a runner - # forever. With the long `family` list and the longest benchmark id, this label is close. runs-on: >- ${{ github.repository == 'vortex-data/vortex' - && format('runs-on={0}/region=us-east-1/runner=bench-dedicated/family=c8gd.8xlarge+c8g.8xlarge+c8gn.8xlarge+m8gd.8xlarge+m8g.8xlarge+r8gd.8xlarge+r8g.8xlarge+x8g.8xlarge+i8g.8xlarge/image=ubuntu26-full-arm64-pre-v2/tag={1}{2}', github.run_id, inputs.benchmark_id, github.event.pull_request.head.repo.fork == false && '/extras=s3-cache' || '') + && format('runs-on={0}/region=us-east-1/runner=arm64-bench/tag={1}{2}', github.run_id, inputs.benchmark_id, github.event.pull_request.head.repo.fork == false && '/extras=s3-cache' || '') || 'ubuntu-latest' }} steps: - uses: runs-on/action@v2 diff --git a/.github/workflows/sql-bench-matrix.yml b/.github/workflows/sql-bench-matrix.yml index 7697aaa9af6..f6bc7d791dd 100644 --- a/.github/workflows/sql-bench-matrix.yml +++ b/.github/workflows/sql-bench-matrix.yml @@ -49,7 +49,7 @@ jobs: FLAT_LAYOUT_INLINE_ARRAY_NODE: "1" runs-on: >- ${{ github.repository == 'vortex-data/vortex' - && format('runs-on={0}/region=us-east-1/runner=bench-dedicated/family=c8gd.8xlarge+c8g.8xlarge+c8gn.8xlarge+m8gd.8xlarge+m8g.8xlarge+r8gd.8xlarge+r8g.8xlarge+x8g.8xlarge+i8g.8xlarge/image=ubuntu26-full-arm64-pre-v2/tag=build{1}', github.run_id, (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) && '/extras=s3-cache' || '') + && format('runs-on={0}/region=us-east-1/runner=arm64-bench/tag=build{1}', github.run_id, (inputs.mode != 'pr' || github.event.pull_request.head.repo.fork == false) && '/extras=s3-cache' || '') || 'ubuntu-latest' }} steps: - uses: runs-on/action@v2 From d5271f90922505545d21e507475834ca7ab3b820 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 07:13:07 -0500 Subject: [PATCH 05/41] Display dates beyond jiff's range without panicking (#11187) ## Summary - Tracking Issue: #11114 `Date` accepts every `i32` day count and every `i64` millisecond count, but jiff only holds the years -9999 to 9999, so formatting a valid `Date` outside that range panicked. `Display` now prints the raw value and unit instead (e.g., `10000000days`), like the `Timestamp` fallback from #10953. ## Changes Tests cover the first date after 9999-12-31, day and millisecond counts that a jiff `Span` can't hold, and an ordinary date whose output doesn't change. Signed-off-by: Connor Tsui --- vortex-array/src/extension/datetime/date.rs | 29 +++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/vortex-array/src/extension/datetime/date.rs b/vortex-array/src/extension/datetime/date.rs index 4504ee8b1ff..ec8b74cb55e 100644 --- a/vortex-array/src/extension/datetime/date.rs +++ b/vortex-array/src/extension/datetime/date.rs @@ -3,7 +3,6 @@ use std::fmt; -use jiff::Span; use vortex_error::VortexExpect; use vortex_error::VortexResult; use vortex_error::vortex_bail; @@ -68,11 +67,20 @@ pub enum DateValue { impl fmt::Display for DateValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let date = match self { - DateValue::Days(days) => EPOCH + Span::new().days(*days), - DateValue::Milliseconds(ms) => EPOCH + Span::new().milliseconds(*ms), + let (unit, value) = match *self { + DateValue::Days(days) => (TimeUnit::Days, i64::from(days)), + DateValue::Milliseconds(ms) => (TimeUnit::Milliseconds, ms), }; - write!(f, "{}", date) + + // jiff cannot hold every storage value, such as any date after the year 9999. + let Ok(date) = unit + .to_jiff_span(value) + .and_then(|span| Ok(EPOCH.checked_add(span)?)) + else { + return write!(f, "{value}{unit}"); + }; + + write!(f, "{date}") } } @@ -128,12 +136,14 @@ impl ExtVTable for Date { #[cfg(test)] mod tests { + use rstest::rstest; use vortex_error::VortexResult; use crate::dtype::DType; use crate::dtype::Nullability::Nullable; use crate::dtype::extension::ExtDType; use crate::extension::datetime::Date; + use crate::extension::datetime::DateValue; use crate::extension::datetime::TimeUnit; use crate::scalar::PValue; use crate::scalar::Scalar; @@ -177,6 +187,15 @@ mod tests { assert_eq!(format!("{}", scalar.as_extension()), "1971-01-01"); } + #[rstest] + #[case::ordinary(DateValue::Milliseconds(86_400_000), "1970-01-02")] + #[case::after_year_9999(DateValue::Days(2_932_897), "2932897days")] + #[case::beyond_jiff_span(DateValue::Days(10_000_000), "10000000days")] + #[case::max_milliseconds(DateValue::Milliseconds(i64::MAX), "9223372036854775807ms")] + fn display_date_value(#[case] value: DateValue, #[case] expected: &str) { + assert_eq!(value.to_string(), expected); + } + #[test] fn deserialize_empty_metadata_returns_error() { use crate::dtype::extension::ExtVTable; From 956c1ea188cb03a92acaed8a7a3bfc71aabe050d Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 07:26:28 -0500 Subject: [PATCH 06/41] Reject nullable nested levels in list-based spatial types (#11183) ## Summary - Tracking Issue: #11114 GeoArrow only allows nulls at the outer level of a geometry, but the validators of the list-based spatial types only checked the coordinate fields, so `LineString` also accepted `list({x=f64, y=f64}?)`. `LineString`, `MultiPoint`, `Polygon`, `MultiLineString`, and `MultiPolygon` now accept only the storage that their helper builds, at either top-level nullability. That leaves each type one storage form for each dimension, which the storage declarations (SD in #11114) need. ## Changes Each validator compares the stored dtype with its storage helper, so the helpers stay the only definition of the form. `Point` and `Rect` already rejected nullable coordinate fields, and WKB has no nested level. Tests cover every nested level of each type, and reading a serialized `vortex.st.linestring` whose coordinate `Struct` is nullable. ## API Changes Building one of these five types with a nullable inner `List` or coordinate `Struct` (e.g., with `ExtDType::try_new`) now returns an error, and so does reading a dtype in that form. No Vortex writer produces that form, so only storage built by hand through the Rust API can hit it. Note that a file keeps its columns as fields of a struct whose field dtypes are parsed lazily, so reading such a column from a file currently panics instead of returning the error. A follow-up fixes that. Signed-off-by: Connor Tsui --- vortex-spatial/src/extension/linestring.rs | 13 +++- vortex-spatial/src/extension/mod.rs | 61 +++++++++++++++++++ .../src/extension/multilinestring.rs | 15 ++++- vortex-spatial/src/extension/multipoint.rs | 14 ++++- vortex-spatial/src/extension/multipolygon.rs | 14 ++++- vortex-spatial/src/extension/polygon.rs | 14 ++++- 6 files changed, 121 insertions(+), 10 deletions(-) diff --git a/vortex-spatial/src/extension/linestring.rs b/vortex-spatial/src/extension/linestring.rs index 0fcd508a63d..9bb122eaf24 100644 --- a/vortex-spatial/src/extension/linestring.rs +++ b/vortex-spatial/src/extension/linestring.rs @@ -104,12 +104,21 @@ pub(crate) fn linestring_storage_dtype(dim: Dimension, nullability: Nullability) DType::List(Arc::new(coords), nullability) } -/// Validate `dtype` is `List` and return its [`Dimension`]. +/// Validate that `dtype` equals [`linestring_storage_dtype`] for its [`Dimension`] and top-level +/// nullability, and return that dimension. As in GeoArrow, only the outer `List` can be nullable. pub(crate) fn linestring_dimension(dtype: &DType) -> VortexResult { let DType::List(coords, _) = dtype else { vortex_bail!("linestring storage must be a List of coordinates, was {dtype}"); }; - coordinate_dimension(coords) + let dimension = coordinate_dimension(coords)?; + + vortex_ensure_eq!( + *dtype, + linestring_storage_dtype(dimension, dtype.nullability()), + "linestring storage can be nullable only at its outer List" + ); + + Ok(dimension) } /// Return one coordinate ordinate, filling an ordinate absent from the point dimension with zero. diff --git a/vortex-spatial/src/extension/mod.rs b/vortex-spatial/src/extension/mod.rs index 99ee4d7ef8f..b31887bd83b 100644 --- a/vortex-spatial/src/extension/mod.rs +++ b/vortex-spatial/src/extension/mod.rs @@ -347,11 +347,18 @@ pub(crate) fn spatial_metadata_from_arrow(metadata: &Metadata) -> SpatialMetadat #[cfg(test)] mod tests { + use std::sync::Arc; use std::sync::LazyLock; use prost::Message; + use rstest::rstest; use vortex_array::VortexSessionExecute; use vortex_array::dtype::DType; + use vortex_array::dtype::Nullability; + use vortex_array::dtype::extension::ExtDType; + use vortex_array::dtype::extension::ExtVTable; + use vortex_array::dtype::proto::dtype as pb; + use vortex_array::dtype::proto::dtype::d_type::DtypeType; use vortex_error::VortexResult; use vortex_error::vortex_err; use vortex_session::VortexSession; @@ -359,9 +366,13 @@ mod tests { use super::LineString; use super::MultiLineString; use super::MultiPoint; + use super::MultiPolygon; use super::Point; use super::Polygon; use crate::extension::SpatialMetadata; + use crate::extension::coordinate::Dimension; + use crate::extension::coordinate::coordinate_storage_dtype; + use crate::test_harness::spatial_session; static SESSION: LazyLock = LazyLock::new(VortexSession::empty); @@ -546,4 +557,54 @@ mod tests { assert_eq!(actual_storage, expected_storage); Ok(()) } + + /// A non-nullable `List` of `element`. + fn list(element: DType) -> DType { + DType::List(Arc::new(element), Nullability::NonNullable) + } + + /// The non-nullable 2D coordinate `Struct`. + fn xy() -> DType { + coordinate_storage_dtype(Dimension::Xy, Nullability::NonNullable) + } + + /// The list-based geometry types reject storage with a nullable inner `List` or coordinate + /// `Struct`, and the error reports the rejected storage. + #[rstest] + #[case::linestring_coordinates( LineString, list(xy().as_nullable()))] + #[case::multipoint_coordinates( MultiPoint, list(xy().as_nullable()))] + #[case::polygon_rings( Polygon, list(list(xy()).as_nullable()))] + #[case::polygon_coordinates( Polygon, list(list(xy().as_nullable())))] + #[case::multilinestring_lines( MultiLineString, list(list(xy()).as_nullable()))] + #[case::multilinestring_coordinates(MultiLineString, list(list(xy().as_nullable())))] + #[case::multipolygon_polygons( MultiPolygon, list(list(list(xy())).as_nullable()))] + #[case::multipolygon_rings( MultiPolygon, list(list(list(xy()).as_nullable())))] + #[case::multipolygon_coordinates( MultiPolygon, list(list(list(xy().as_nullable()))))] + fn nullable_nested_level_is_rejected( + #[case] vtable: impl ExtVTable, + #[case] storage: DType, + ) { + let result = ExtDType::try_with_vtable(vtable, SpatialMetadata::default(), storage.clone()); + assert!(result.is_err_and(|error| error.to_string().contains(&format!("left: {storage}")))); + } + + /// A serialized `vortex.st.linestring` dtype with a nullable coordinate `Struct` fails to read, + /// with an error that names the expected storage. [`LineString`] rejects that storage at + /// construction, so the test builds the serialized extension dtype by hand. + #[test] + fn nullable_nested_level_fails_to_read() -> VortexResult<()> { + let serialized = pb::DType { + dtype_type: Some(DtypeType::Extension(Box::new(pb::Extension { + id: "vortex.st.linestring".to_string(), + storage_dtype: Some(Box::new(pb::DType::try_from(&list(xy().as_nullable()))?)), + metadata: None, + }))), + }; + + let result = DType::from_proto(&serialized, &spatial_session()); + assert!(result.is_err_and(|error| error.to_string().contains( + "linestring storage can be nullable only at its outer List\n left: list({x=f64, y=f64}?)\n right: list({x=f64, y=f64})" + ))); + Ok(()) + } } diff --git a/vortex-spatial/src/extension/multilinestring.rs b/vortex-spatial/src/extension/multilinestring.rs index 0443f615211..404f3656f5c 100644 --- a/vortex-spatial/src/extension/multilinestring.rs +++ b/vortex-spatial/src/extension/multilinestring.rs @@ -42,6 +42,7 @@ use vortex_error::VortexError; use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_error::vortex_ensure; +use vortex_error::vortex_ensure_eq; use vortex_error::vortex_err; use vortex_session::registry::CachedId; use vortex_session::registry::Id; @@ -97,7 +98,9 @@ pub(crate) fn multilinestring_storage_dtype(dim: Dimension, nullability: Nullabi DType::List(Arc::new(line), nullability) } -/// Validate `dtype` is `List>` and return its [`Dimension`]. +/// Validate that `dtype` equals [`multilinestring_storage_dtype`] for its [`Dimension`] and +/// top-level nullability, and return that dimension. As in GeoArrow, only the outer `List` can be +/// nullable. pub(crate) fn multilinestring_dimension(dtype: &DType) -> VortexResult { let DType::List(line, _) = dtype else { vortex_bail!("multilinestring storage must be a List of line strings, was {dtype}"); @@ -105,7 +108,15 @@ pub(crate) fn multilinestring_dimension(dtype: &DType) -> VortexResult` and return its [`Dimension`]. +/// Validate that `dtype` equals [`multipoint_storage_dtype`] for its [`Dimension`] and top-level +/// nullability, and return that dimension. As in GeoArrow, only the outer `List` can be nullable. pub(crate) fn multipoint_dimension(dtype: &DType) -> VortexResult { let DType::List(coords, _) = dtype else { vortex_bail!("multipoint storage must be a List of coordinates, was {dtype}"); }; - coordinate_dimension(coords) + let dimension = coordinate_dimension(coords)?; + + vortex_ensure_eq!( + *dtype, + multipoint_storage_dtype(dimension, dtype.nullability()), + "multipoint storage can be nullable only at its outer List" + ); + + Ok(dimension) } static ARROW_MULTIPOINT: CachedId = CachedId::new(MultiPointType::NAME); diff --git a/vortex-spatial/src/extension/multipolygon.rs b/vortex-spatial/src/extension/multipolygon.rs index c7e88c4b71b..9f67ce94747 100644 --- a/vortex-spatial/src/extension/multipolygon.rs +++ b/vortex-spatial/src/extension/multipolygon.rs @@ -41,6 +41,7 @@ use vortex_error::VortexError; use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_error::vortex_ensure; +use vortex_error::vortex_ensure_eq; use vortex_error::vortex_err; use vortex_session::registry::CachedId; use vortex_session::registry::Id; @@ -95,7 +96,8 @@ pub(crate) fn multipolygon_storage_dtype(dim: Dimension, nullability: Nullabilit DType::List(Arc::new(polygon), nullability) } -/// Validate `dtype` is `List>>` and return its [`Dimension`]. +/// Validate that `dtype` equals [`multipolygon_storage_dtype`] for its [`Dimension`] and top-level +/// nullability, and return that dimension. As in GeoArrow, only the outer `List` can be nullable. pub(crate) fn multipolygon_dimension(dtype: &DType) -> VortexResult { let DType::List(polygon, _) = dtype else { vortex_bail!("multipolygon storage must be a List of polygons, was {dtype}"); @@ -106,7 +108,15 @@ pub(crate) fn multipolygon_dimension(dtype: &DType) -> VortexResult { let DType::List(coords, _) = ring.as_ref() else { vortex_bail!("multipolygon ring storage must be a List of coordinates, was {ring}"); }; - coordinate_dimension(coords) + let dimension = coordinate_dimension(coords)?; + + vortex_ensure_eq!( + *dtype, + multipolygon_storage_dtype(dimension, dtype.nullability()), + "multipolygon storage can be nullable only at its outer List" + ); + + Ok(dimension) } static ARROW_MULTIPOLYGON: CachedId = CachedId::new(MultiPolygonType::NAME); diff --git a/vortex-spatial/src/extension/polygon.rs b/vortex-spatial/src/extension/polygon.rs index 4d2f632c521..0bfbe101996 100644 --- a/vortex-spatial/src/extension/polygon.rs +++ b/vortex-spatial/src/extension/polygon.rs @@ -47,6 +47,7 @@ use vortex_error::VortexError; use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_error::vortex_ensure; +use vortex_error::vortex_ensure_eq; use vortex_error::vortex_err; use vortex_session::registry::CachedId; use vortex_session::registry::Id; @@ -100,7 +101,8 @@ pub(crate) fn polygon_storage_dtype(dim: Dimension, nullability: Nullability) -> DType::List(Arc::new(ring), nullability) } -/// Validate `dtype` is `List>` and return its [`Dimension`]. +/// Validate that `dtype` equals [`polygon_storage_dtype`] for its [`Dimension`] and top-level +/// nullability, and return that dimension. As in GeoArrow, only the outer `List` can be nullable. pub(crate) fn polygon_dimension(dtype: &DType) -> VortexResult { let DType::List(ring, _) = dtype else { vortex_bail!("polygon storage must be a List of rings, was {dtype}"); @@ -108,7 +110,15 @@ pub(crate) fn polygon_dimension(dtype: &DType) -> VortexResult { let DType::List(coords, _) = ring.as_ref() else { vortex_bail!("polygon ring storage must be a List of coordinates, was {ring}"); }; - coordinate_dimension(coords) + let dimension = coordinate_dimension(coords)?; + + vortex_ensure_eq!( + *dtype, + polygon_storage_dtype(dimension, dtype.nullability()), + "polygon storage can be nullable only at its outer List" + ); + + Ok(dimension) } static ARROW_POLYGON: CachedId = CachedId::new(PolygonType::NAME); From 0151998e5da218acdf23e4f97ee945d35899a43c Mon Sep 17 00:00:00 2001 From: Alexander Droste Date: Wed, 4 Nov 2026 13:27:04 +0100 Subject: [PATCH 07/41] refactor(cuda): slice mutable views through one implementation (#10880) ## Summary Removes duplicate mutable-view slicing logic by making CudaViewMut::slice_mut delegate to into_slice_mut. The temporary copies only view metadata, not device data. The return lifetime keeps the original view mutably borrowed while the result lives. Bounds checks, panic messages, pointer arithmetic, and access tracking remain unchanged. ## Changes - refactor(cuda): slice mutable views through one implementation - docs(cuda): clarify mutable view borrow lifetime No performance impact is expected: this adds no allocation, CUDA calls, synchronization, or device-data copies. Optimized builds should eliminate the temporary view and inline the helper. No builds, tests, or benchmarks were run for this change. --------- Signed-off-by: Alexander Droste --- vortex-cuda/driver/src/memory.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/vortex-cuda/driver/src/memory.rs b/vortex-cuda/driver/src/memory.rs index 4db0eb9c35b..ed86152e06b 100644 --- a/vortex-cuda/driver/src/memory.rs +++ b/vortex-cuda/driver/src/memory.rs @@ -705,14 +705,8 @@ impl<'a, T> CudaViewMut<'a, T> { /// /// Panics if `bounds` is out of range. pub fn slice_mut(&mut self, bounds: impl RangeBounds) -> CudaViewMut<'_, T> { - let (start, end) = range(&bounds, self.len).expect("CUDA view range out of bounds"); - CudaViewMut { - pointer: self.pointer + (start * size_of::()) as u64, - len: end - start, - access: self.access, - stream: self.stream, - marker: PhantomData, - } + // The return lifetime keeps `self` mutably borrowed while the copied view lives. + CudaViewMut { ..*self }.into_slice_mut(bounds) } fn into_slice_mut(self, bounds: impl RangeBounds) -> CudaViewMut<'a, T> { From a52afdbfa8908cf47a57b2b62a842d2b3c1bcccb Mon Sep 17 00:00:00 2001 From: Alexander Droste Date: Wed, 4 Nov 2026 14:31:49 +0100 Subject: [PATCH 08/41] docs(cuda): clarify mapped-page eviction (#10882) ## Summary Corrects the documentation for release_file_mappings and vx_cuda_release_file_mappings. Keeping a file mapped does not pin its pages in memory: memory pressure can still reclaim them. On Linux, both posix_fadvise(POSIX_FADV_DONTNEED) and /proc/sys/vm/drop_caches skip pages mapped into a process. Release Vortex's cached mappings before using either for cold-cache measurements. Mappings still used by open readers remain mapped until those readers are released. ## Changes - Replace the claim that mapped pages cannot be evicted with the distinction between explicit cache eviction and memory-pressure reclaim. - Document both Linux eviction methods. - Include the memory-pressure caveat in both Rust and FFI documentation. - Regenerate the C header through cbindgen to match the FFI docs. Documentation only; no runtime behavior changes. No builds, tests, or benchmarks were run. --------- Signed-off-by: Alexander Droste --- vortex-cuda/ffi/cinclude/vortex_cuda.h | 10 ++++++---- vortex-cuda/ffi/src/lib.rs | 10 ++++++---- vortex-cuda/src/pooled_read_at/file.rs | 9 +++++---- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/vortex-cuda/ffi/cinclude/vortex_cuda.h b/vortex-cuda/ffi/cinclude/vortex_cuda.h index 5daccce6663..2d15a5eb12d 100644 --- a/vortex-cuda/ffi/cinclude/vortex_cuda.h +++ b/vortex-cuda/ffi/cinclude/vortex_cuda.h @@ -89,10 +89,12 @@ extern "C" { /** * Release Vortex's cached file mappings that no open scan uses. * - * Scans keep their files mapped so later scans of the same file reuse filled page tables. The - * operating system cannot evict mapped pages from the page cache, so call this before evicting - * files, for example before a cold-cache measurement. Mappings still used by open streams stay - * mapped until those streams are released. + * Scans keep their files mapped so later scans of the same file reuse filled page tables. + * On Linux, both `posix_fadvise(POSIX_FADV_DONTNEED)` and `/proc/sys/vm/drop_caches` + * skip pages mapped into a process, although memory pressure can still reclaim them. + * Call this before using either to evict file pages, for example before a cold-cache + * measurement. Mappings still used by open streams stay mapped until those streams + * are released. */ void vx_cuda_release_file_mappings(void); diff --git a/vortex-cuda/ffi/src/lib.rs b/vortex-cuda/ffi/src/lib.rs index aa41476b405..3b8f5a45c33 100644 --- a/vortex-cuda/ffi/src/lib.rs +++ b/vortex-cuda/ffi/src/lib.rs @@ -79,10 +79,12 @@ pub struct vx_cuda_scan_options { /// Release Vortex's cached file mappings that no open scan uses. /// -/// Scans keep their files mapped so later scans of the same file reuse filled page tables. The -/// operating system cannot evict mapped pages from the page cache, so call this before evicting -/// files, for example before a cold-cache measurement. Mappings still used by open streams stay -/// mapped until those streams are released. +/// Scans keep their files mapped so later scans of the same file reuse filled page tables. +/// On Linux, both `posix_fadvise(POSIX_FADV_DONTNEED)` and `/proc/sys/vm/drop_caches` +/// skip pages mapped into a process, although memory pressure can still reclaim them. +/// Call this before using either to evict file pages, for example before a cold-cache +/// measurement. Mappings still used by open streams stay mapped until those streams +/// are released. #[unsafe(no_mangle)] pub extern "C-unwind" fn vx_cuda_release_file_mappings() { vortex_cuda::release_file_mappings(); diff --git a/vortex-cuda/src/pooled_read_at/file.rs b/vortex-cuda/src/pooled_read_at/file.rs index 237a3bb91fc..3d07485c211 100644 --- a/vortex-cuda/src/pooled_read_at/file.rs +++ b/vortex-cuda/src/pooled_read_at/file.rs @@ -245,10 +245,11 @@ impl FileMapping { /// Release cached file mappings that no open reader uses, unmapping them before returning. /// /// Readers keep files mapped between scans, so later scans of a file reuse its filled page -/// tables. A mapped page cannot be dropped from the page cache, for example by -/// `posix_fadvise(DONTNEED)`, so release the mappings before evicting files, such as before a -/// cold-cache measurement, or to return the address space. Mappings that open readers still use -/// stay mapped until those readers close. +/// tables. On Linux, both `posix_fadvise(POSIX_FADV_DONTNEED)` and `/proc/sys/vm/drop_caches` +/// skip pages mapped into a process, although memory pressure can still reclaim them. Release +/// the mappings before using either to evict file pages, such as before a cold-cache measurement, +/// or to return the address space. Mappings that open readers still use stay mapped until those +/// readers close. pub fn release_file_mappings() { // Closed readers hand their mappings to the release queue; wait until it has dropped them. if let Some(queue) = MAPPED_RELEASE_QUEUE.as_ref() { From 89af043fb7d173f6132f6d31d94b43b73f79c259 Mon Sep 17 00:00:00 2001 From: Alexander Droste Date: Wed, 4 Nov 2026 14:35:59 +0100 Subject: [PATCH 09/41] test(cuda): encode ALP test inputs with the test's own context (#10881) ## Summary Reuses each test's existing host execution context when encoding inputs in the CUDA ALP and FOR tests, instead of creating a temporary context from the same session. This is test-only cleanup. Input values, assertions, and GPU execution contexts remain unchanged; production code is unaffected. ## Changes - Reuse the existing host context in five ALP tests. - Apply the same cleanup to the signed FOR and patched FOR tests. No builds, tests, or benchmarks were run for this change. --------- Signed-off-by: Alexander Droste --- vortex-cuda/src/kernel/encodings/alp.rs | 10 +++++----- vortex-cuda/src/kernel/encodings/for_.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vortex-cuda/src/kernel/encodings/alp.rs b/vortex-cuda/src/kernel/encodings/alp.rs index 5019b56f8c7..4aa4ebc901c 100644 --- a/vortex-cuda/src/kernel/encodings/alp.rs +++ b/vortex-cuda/src/kernel/encodings/alp.rs @@ -248,7 +248,7 @@ mod tests { Some(5.0), ]; let prim = PrimitiveArray::from_option_iter(values); - let alp_array = alp_encode(prim.as_view(), None, &mut SESSION.create_execution_ctx())?; + let alp_array = alp_encode(prim.as_view(), None, &mut ctx)?; let gpu_result = alp_array .clone() @@ -279,7 +279,7 @@ mod tests { Buffer::from(vec![1.0f32, 2.0, 3.0, 4.0, 5.0]), Validity::AllValid, ); - let alp_array = alp_encode(values.as_view(), None, &mut SESSION.create_execution_ctx())?; + let alp_array = alp_encode(values.as_view(), None, &mut ctx)?; let gpu_result = alp_array .clone() @@ -323,7 +323,7 @@ mod tests { }) .collect(); let prim = PrimitiveArray::new(values, Validity::NonNullable); - let alp_array = alp_encode(prim.as_view(), None, &mut SESSION.create_execution_ctx())?; + let alp_array = alp_encode(prim.as_view(), None, &mut ctx)?; assert!( alp_array.patches().is_some(), "expected patches from ALP exceptions" @@ -368,7 +368,7 @@ mod tests { }) .collect(); let prim = PrimitiveArray::new(values, Validity::NonNullable); - let alp_array = alp_encode(prim.as_view(), None, &mut SESSION.create_execution_ctx())?; + let alp_array = alp_encode(prim.as_view(), None, &mut ctx)?; assert!( alp_array.patches().is_some(), "expected patches from ALP exceptions" @@ -460,7 +460,7 @@ mod tests { .map(|i| if i == 1400 { UNENCODABLE } else { i as f64 }) .collect(); let prim = PrimitiveArray::new(values, Validity::NonNullable); - let alp_array = alp_encode(prim.as_view(), None, &mut SESSION.create_execution_ctx())?; + let alp_array = alp_encode(prim.as_view(), None, &mut ctx)?; assert!( alp_array.patches().is_some(), "expected patches from ALP exceptions" diff --git a/vortex-cuda/src/kernel/encodings/for_.rs b/vortex-cuda/src/kernel/encodings/for_.rs index 52dbfd377a6..bd29a7159c7 100644 --- a/vortex-cuda/src/kernel/encodings/for_.rs +++ b/vortex-cuda/src/kernel/encodings/for_.rs @@ -232,7 +232,7 @@ mod tests { .take(1024) .collect::>() .into_array(); - let packed = BitPacked::encode(&values, 3, &mut SESSION.create_execution_ctx()) + let packed = BitPacked::encode(&values, 3, &mut ctx) .unwrap() .into_array(); let for_array = FoR::try_new(packed, (-8i8).into()).unwrap(); @@ -275,7 +275,7 @@ mod tests { } let values = PrimitiveArray::new(Buffer::from(values), NonNullable).into_array(); - let packed = BitPacked::encode(&values, 8, &mut SESSION.create_execution_ctx())?; + let packed = BitPacked::encode(&values, 8, &mut ctx)?; assert!( packed.patches().is_some(), "test setup expects the exceptions to be stored as patches" From 531d1267e72746058be05e35f496abb05092edde Mon Sep 17 00:00:00 2001 From: Alexander Droste Date: Wed, 4 Nov 2026 14:39:02 +0100 Subject: [PATCH 10/41] refactor(cuda): avoid access tracking for address-only lookups (#10884) ## Summary Adds CudaSlice::device_address for callers that only need an allocation's address, without ordering or recording a device access. Use it when building output-pointer metadata in FSST, OnPair, and Zstd, and for address checks in dynamic-dispatch tests. Actual GPU reads and writes remain tracked through launch arguments or device-pointer guards. ## Changes - Add and document the untracked device_address accessor. - Replace address-only device_ptr calls that immediately discarded their access guards. - Keep the tracked read in CudaDeviceBuffer::new: it orders the allocation stream after pending writes on other streams. Document why its guard can be dropped immediately. - Simplify the Zstd comments identifying where output writes are tracked. Each replaced lookup avoids two CUDA context checks, two access-state mutex acquisitions, and unnecessary read-tracking updates. These sites did not require cross-stream waits, so this removes bookkeeping rather than synchronization needed by the workload. No builds, tests, or benchmarks were run for this change. --------- Signed-off-by: Alexander Droste --- vortex-cuda/driver/src/memory.rs | 8 ++++++++ vortex-cuda/src/device_buffer.rs | 2 ++ vortex-cuda/src/dynamic_dispatch/mod.rs | 6 +++--- vortex-cuda/src/kernel/encodings/fsst.rs | 5 ++--- vortex-cuda/src/kernel/encodings/onpair.rs | 3 +-- vortex-cuda/src/kernel/encodings/zstd.rs | 6 ++---- vortex-cuda/src/kernel/encodings/zstd_buffers.rs | 6 ++---- 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/vortex-cuda/driver/src/memory.rs b/vortex-cuda/driver/src/memory.rs index ed86152e06b..38dab643873 100644 --- a/vortex-cuda/driver/src/memory.rs +++ b/vortex-cuda/driver/src/memory.rs @@ -445,6 +445,14 @@ impl CudaSlice { &self.stream } + /// The device address, to inspect or compute pointers from without accessing the memory. + /// + /// Unlike [`DevicePtr::device_ptr`], this orders nothing: work using the address must be + /// ordered after the memory's other accesses, for example through a device pointer guard. + pub fn device_address(&self) -> sys::CUdeviceptr { + self.pointer + } + /// The whole slice as a view. pub fn as_view(&self) -> CudaView<'_, T> { CudaView { diff --git a/vortex-cuda/src/device_buffer.rs b/vortex-cuda/src/device_buffer.rs index 2729c992115..322ca0fbb63 100644 --- a/vortex-cuda/src/device_buffer.rs +++ b/vortex-cuda/src/device_buffer.rs @@ -115,6 +115,8 @@ impl CudaDeviceBuffer { /// `BufferHandle` interface that it implements. pub fn new(cuda_slice: CudaSlice) -> Self { let len = cuda_slice.len() * size_of::(); + // Keep the tracked read to order the allocation stream after writes on other streams. + // Only the address is stored here, so drop the guard immediately; queued waits remain. let (device_ptr, _) = cuda_slice.device_ptr(cuda_slice.stream()); Self { diff --git a/vortex-cuda/src/dynamic_dispatch/mod.rs b/vortex-cuda/src/dynamic_dispatch/mod.rs index c05a513da13..b8577ab55f3 100644 --- a/vortex-cuda/src/dynamic_dispatch/mod.rs +++ b/vortex-cuda/src/dynamic_dispatch/mod.rs @@ -640,7 +640,7 @@ mod tests { .clone_htod(data) .map_err(|e| vortex_err!("htod: {e}"))?, ); - let (ptr, _) = device_buf.device_ptr(cuda_ctx.stream()); + let ptr = device_buf.device_address(); Ok((ptr, device_buf)) } @@ -2310,7 +2310,7 @@ mod tests { .clone_htod(&i8_values) .map_err(|e| vortex_err!("htod: {e}"))?, ); - let (input_ptr, _) = device_buf.device_ptr(cuda_ctx.stream()); + let input_ptr = device_buf.device_address(); // Build a single-stage LOAD plan: source ptype = I8, output ptype = U32. // The kernel (instantiated as u32) must sign-extend each i8 element. @@ -2348,7 +2348,7 @@ mod tests { .clone_htod(&i16_values) .map_err(|e| vortex_err!("htod: {e}"))?, ); - let (input_ptr, _) = device_buf.device_ptr(cuda_ctx.stream()); + let input_ptr = device_buf.device_address(); let plan = CudaDispatchPlan::new( [MaterializedStage::new( diff --git a/vortex-cuda/src/kernel/encodings/fsst.rs b/vortex-cuda/src/kernel/encodings/fsst.rs index 0a0e91d1b54..ae576e6b50d 100644 --- a/vortex-cuda/src/kernel/encodings/fsst.rs +++ b/vortex-cuda/src/kernel/encodings/fsst.rs @@ -34,7 +34,6 @@ use vortex::encodings::fsst::FSSTArraySlotsExt; use vortex::error::VortexExpect; use vortex::error::VortexResult; use vortex::error::vortex_err; -use vortex_cuda_driver::DevicePtr; use vortex_cuda_driver::DeviceRepr; use vortex_cuda_driver::PushKernelArg; @@ -242,7 +241,7 @@ where // The kernel gates store widths on `out_pos % N` relative to the base, so the base must // satisfy the widest store (u128 → 16). let mut output = ctx.device_alloc::(total_size)?; - let (output_base_ptr, _) = output.device_ptr(ctx.stream()); + let output_base_ptr = output.device_address(); assert_eq!( output_base_ptr % 16, 0, @@ -336,7 +335,7 @@ where let mut device_views = (total_size <= MAX_BUFFER_LEN) .then(|| ctx.device_alloc::(num_strings)) .transpose()?; - let (output_base_ptr, _) = device_output.device_ptr(ctx.stream()); + let output_base_ptr = device_output.device_address(); assert_eq!( output_base_ptr % 16, 0, diff --git a/vortex-cuda/src/kernel/encodings/onpair.rs b/vortex-cuda/src/kernel/encodings/onpair.rs index c511419eb97..639b9f195d3 100644 --- a/vortex-cuda/src/kernel/encodings/onpair.rs +++ b/vortex-cuda/src/kernel/encodings/onpair.rs @@ -67,7 +67,6 @@ use vortex::error::vortex_ensure_eq; use vortex::error::vortex_err; use vortex_array::ArrayView; use vortex_cuda_driver::CudaSlice; -use vortex_cuda_driver::DevicePtr; use vortex_cuda_driver::DeviceRepr; use vortex_cuda_driver::LaunchConfig; use vortex_cuda_driver::PushKernelArg; @@ -496,7 +495,7 @@ where // Decode only batches intersecting the visible token window. The kernel's drain gates 16-byte // stores on `out_start % 16` relative to the buffer base, so the base must be 16-aligned. let mut bytes = ctx.device_alloc::(total_size)?; - let (bytes_base_ptr, _) = bytes.device_ptr(ctx.stream()); + let bytes_base_ptr = bytes.device_address(); assert_eq!( bytes_base_ptr % 16, 0, diff --git a/vortex-cuda/src/kernel/encodings/zstd.rs b/vortex-cuda/src/kernel/encodings/zstd.rs index 42847f51965..07cdd8ab7b3 100644 --- a/vortex-cuda/src/kernel/encodings/zstd.rs +++ b/vortex-cuda/src/kernel/encodings/zstd.rs @@ -126,10 +126,8 @@ pub async fn zstd_kernel_prepare( // Build output_ptrs from output base pointer + offsets. let output_ptrs = { - // We only need the allocation address here to build pointer metadata. - // The actual device write is tracked by `record_device_output` around - // `decompress_async`, so this guard can be dropped immediately. - let (base_ptr, _) = device_output.device_ptr(ctx.stream()); + // The write is tracked by `record_device_output` around `decompress_async`. + let base_ptr = device_output.device_address(); output_sizes .iter() .scan(0u64, |offset, &size| { diff --git a/vortex-cuda/src/kernel/encodings/zstd_buffers.rs b/vortex-cuda/src/kernel/encodings/zstd_buffers.rs index 79d0b36efb9..3180a7bb0cd 100644 --- a/vortex-cuda/src/kernel/encodings/zstd_buffers.rs +++ b/vortex-cuda/src/kernel/encodings/zstd_buffers.rs @@ -83,10 +83,8 @@ async fn decode_zstd_buffers( } let output_ptrs = { - // We only need the allocation address to assemble output pointer metadata. - // The actual device write is tracked by `record_device_output` around - // `decompress_async`, so this guard can be dropped immediately. - let (base_ptr, _) = device_output.device_ptr(ctx.stream()); + // The write is tracked by `record_device_output` around `decompress_async`. + let base_ptr = device_output.device_address(); plan.output_offsets() .iter() .map(|offset| base_ptr + *offset as u64) From a10a0e0819f518629a3b87bf5efc55369bd1f91a Mon Sep 17 00:00:00 2001 From: Frederic Branczyk Date: Wed, 4 Nov 2026 14:52:21 +0100 Subject: [PATCH 11/41] Expose the existing footer segment end offset (#11196) Signed-off-by: Frederic Branczyk --- vortex-file/src/footer/mod.rs | 30 +++++++++++++++++ vortex-file/src/tests.rs | 61 +++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/vortex-file/src/footer/mod.rs b/vortex-file/src/footer/mod.rs index 567cc3cbfba..a677a6e1063 100644 --- a/vortex-file/src/footer/mod.rs +++ b/vortex-file/src/footer/mod.rs @@ -177,10 +177,24 @@ impl Footer { } /// Returns the segment map of the file. + /// + /// Segment IDs in the [`Self::layout`] tree index this map. This includes segments in + /// auxiliary `zones` children of zoned layouts; walk the child's subtree to find all of its + /// segments, since a zone map need not occupy a single segment. pub fn segment_map(&self) -> &Arc<[SegmentSpec]> { &self.segments } + /// Returns the largest exclusive byte end of the data and user-defined metadata segments. + /// + /// Returns `Some(0)` when there are no segments, or `None` if any segment's end overflows + /// `u64`. For files produced by the Vortex writer, reading from this offset through the end + /// of the file includes the serialized footer. User-defined metadata bytes precede this + /// offset; their locations are available through [`Self::metadata_segments`]. + pub fn segments_end(&self) -> Option { + self.segments_end + } + /// Returns the statistics of the file. pub fn statistics(&self) -> Option<&FileStatistics> { self.statistics.as_ref() @@ -386,6 +400,22 @@ mod tests { assert!(err.to_string().contains("past the end"), "{err}"); } + #[test] + fn segments_end_includes_data_and_metadata() { + assert_eq!( + footer_with_metadata_segment(segment(50, 1)).segments_end(), + Some(100) + ); + assert_eq!( + footer_with_metadata_segment(segment(100, 50)).segments_end(), + Some(150) + ); + assert_eq!( + footer_with_metadata_segment(segment(u64::MAX, 1)).segments_end(), + None + ); + } + #[test] fn accepts_metadata_segment_within_file() -> VortexResult<()> { footer_with_metadata_segment(segment(100, 50)).validate_file_size(150)?; diff --git a/vortex-file/src/tests.rs b/vortex-file/src/tests.rs index 5198c6c9c97..c015c706b7d 100644 --- a/vortex-file/src/tests.rs +++ b/vortex-file/src/tests.rs @@ -2610,6 +2610,25 @@ async fn test_file_metadata_roundtrip() -> VortexResult<()> { assert_eq!(default.metadata_segments().count(), 0); assert!(default.metadata_segment("json").is_none()); + let segments_end = summary + .footer() + .segments_end() + .expect("valid segment ranges"); + assert_eq!( + segments_end, + summary.footer().metadata_segment("large").unwrap().offset + large.len() as u64 + ); + let mut deserializer = crate::Footer::deserializer( + ByteBuffer::copy_from(&bytes[segments_end as usize..]), + SESSION.clone(), + ) + .with_size(summary.size()); + let crate::footer::DeserializeStep::Done(footer) = deserializer.deserialize()? else { + panic!("the tail after the segments must contain the footer"); + }; + assert_eq!(footer.segments_end(), Some(segments_end)); + assert_eq!(footer.metadata_segments().count(), 4); + let file = SESSION .open_options() .include_metadata() @@ -3125,9 +3144,14 @@ async fn probe_compressor_override_is_independent() -> VortexResult<()> { Ok(()) } +#[rstest] +#[case::embedded_dtype(false)] +#[case::external_dtype(true)] #[tokio::test] #[cfg_attr(miri, ignore)] -async fn test_segment_ordering_zonemaps_after_data() -> VortexResult<()> { +async fn test_segment_ordering_zonemaps_after_data( + #[case] exclude_dtype: bool, +) -> VortexResult<()> { // Create a multi-column struct with enough rows to produce zone maps. let n = 100_000; let values: Vec<&str> = (0..n).map(|i| ["alpha", "beta", "gamma"][i % 3]).collect(); @@ -3140,10 +3164,14 @@ async fn test_segment_ordering_zonemaps_after_data() -> VortexResult<()> { ("numbers", numbers), ("floats", floats), ])?; + let dtype = st.dtype().clone(); let mut buf = ByteBufferMut::empty(); - let summary = SESSION - .write_options() + let mut options = SESSION.write_options(); + if exclude_dtype { + options = options.exclude_dtype(); + } + let summary = options .write(&mut buf, st.into_array().to_array_stream()) .await?; @@ -3215,6 +3243,33 @@ async fn test_segment_ordering_zonemaps_after_data() -> VortexResult<()> { "global: all data segments should come before all zone map segments", ); + let segments_end = footer.segments_end().expect("valid segment ranges"); + assert!( + all_zones_offsets + .iter() + .all(|offset| *offset < segments_end) + ); + assert_eq!(buf.len() as u64, summary.size()); + let mut deserializer = crate::Footer::deserializer( + ByteBuffer::copy_from(&buf[segments_end as usize..]), + SESSION.clone(), + ) + .with_size(summary.size()) + .with_some_dtype(exclude_dtype.then_some(dtype)); + let crate::footer::DeserializeStep::Done(footer) = deserializer.deserialize()? else { + panic!("the tail after the segments must contain the footer"); + }; + let mut recovered_data_offsets = Vec::new(); + let mut recovered_zones_offsets = Vec::new(); + collect_all_zoned( + footer.layout().as_ref(), + footer.segment_map(), + &mut recovered_data_offsets, + &mut recovered_zones_offsets, + ); + assert_eq!(recovered_data_offsets, all_data_offsets); + assert_eq!(recovered_zones_offsets, all_zones_offsets); + Ok(()) } From 58ceb898e9c3806687de57acc72f0e5c8c6e0058 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 08:53:36 -0500 Subject: [PATCH 12/41] Add shared conversion to canonical decimal arrays or Widen (#11005) Signed-off-by: Connor Tsui --- vortex-array/src/arrays/decimal/array.rs | 40 +- vortex-array/src/arrays/decimal/vtable/mod.rs | 6 +- .../src/arrays/widen/compute/aggregate.rs | 3 + .../src/arrays/widen/compute/fill_null.rs | 2 +- vortex-array/src/arrays/widen/compute/mod.rs | 25 +- .../src/arrays/widen/compute/numeric.rs | 6 +- vortex-array/src/arrays/widen/compute/zip.rs | 2 +- vortex-array/src/arrays/widen/encode.rs | 206 ---------- .../src/arrays/widen/encode/decimal.rs | 338 ++++++++++++++++ .../src/arrays/widen/encode/integer.rs | 125 ++++++ vortex-array/src/arrays/widen/encode/mod.rs | 15 + vortex-array/src/arrays/widen/mod.rs | 5 +- .../src/arrays/widen/tests/decimal_stored.rs | 373 ++++++++++++++++++ vortex-array/src/arrays/widen/tests/mod.rs | 1 + .../src/arrays/widen/vtable/plugin.rs | 3 + 15 files changed, 910 insertions(+), 240 deletions(-) delete mode 100644 vortex-array/src/arrays/widen/encode.rs create mode 100644 vortex-array/src/arrays/widen/encode/decimal.rs create mode 100644 vortex-array/src/arrays/widen/encode/integer.rs create mode 100644 vortex-array/src/arrays/widen/encode/mod.rs create mode 100644 vortex-array/src/arrays/widen/tests/decimal_stored.rs diff --git a/vortex-array/src/arrays/decimal/array.rs b/vortex-array/src/arrays/decimal/array.rs index 91521aab93e..5c83b113c9f 100644 --- a/vortex-array/src/arrays/decimal/array.rs +++ b/vortex-array/src/arrays/decimal/array.rs @@ -59,7 +59,7 @@ pub struct DecimalSlots { /// /// Decimals are stored as scaled integers in a supported scalar value type. /// -/// The precisions supported for each scalar type are: +/// The canonical widths for each precision are: /// - **i8**: precision 1-2 digits /// - **i16**: precision 3-4 digits /// - **i32**: precision 5-9 digits @@ -67,12 +67,12 @@ pub struct DecimalSlots { /// - **i128**: precision 19-38 digits /// - **i256**: precision 39-76 digits /// -/// These are just the maximal ranges for each scalar type, but it is perfectly legal to store -/// values with precision that does not match this exactly. For example, a valid DecimalArray with -/// precision=39 may store its values in an `i8` if all of the actual values fit into it. -/// -/// Similarly, a `DecimalArray` can be built that stores a set of precision=2 values in a -/// `Buffer`. +/// Storage width is still independent of precision during the canonical decimal migration. +/// Existing `vortex.decimal` nodes and DecimalByteParts assembly can produce noncanonical storage. +/// For example, an array with precision 39 can currently use an `i8` buffer when all values fit, +/// while an array with precision 2 can use a `Buffer`. [`DecimalDataParts`] preserves the +/// actual width for conversion into canonical decimal leaves, optionally under +/// [`Widen`](crate::arrays::Widen). /// /// ## Precision and Scale /// @@ -86,7 +86,7 @@ pub struct DecimalSlots { /// /// ## Valid Scalar Types /// -/// The underlying storage uses these native types based on precision: +/// Canonical decimal leaves select one of these native types from precision: /// - `DecimalType::I8`, `I16`, `I32`, `I64`, `I128`, `I256` /// - Type selection is automatic based on the required precision /// @@ -111,6 +111,7 @@ pub struct DecimalSlots { pub struct DecimalData { pub(super) decimal_dtype: DecimalDType, pub(super) values: BufferHandle, + /// Actual storage type, including noncanonical widths still accepted during the migration. pub(super) values_type: DecimalType, } @@ -124,10 +125,21 @@ impl Display for DecimalData { } } +/// Decimal storage and validity, as returned by [`DecimalArray::into_data_parts`]. pub struct DecimalDataParts { + /// The precision and scale of the values. pub decimal_dtype: DecimalDType, + /// Unscaled integers of type `values_type` in host or device memory. pub values: BufferHandle, + /// Actual storage type, retained to interpret input before canonical conversion. + /// + /// Legacy `vortex.decimal` nodes and DecimalByteParts assembly can use a different width from + /// [`DecimalDType::canonical_values_type`]. Comparing the widths identifies noncanonical + /// storage, not the age of the data: current [`WidenPlugin`] writes use that wire form too. + /// + /// [`WidenPlugin`]: crate::arrays::widen::WidenPlugin pub values_type: DecimalType, + /// The validity of the values, which also gives the nullability of the array. pub validity: Validity, } @@ -235,7 +247,8 @@ impl DecimalData { /// /// # Errors /// - /// See [`DecimalArray::new_unchecked`] for invariants that are checked. + /// Returns an error if the buffer length is not a multiple of the width of `values_type`, or + /// if the buffer is not aligned to that width. pub fn try_new_handle( values: BufferHandle, values_type: DecimalType, @@ -243,7 +256,8 @@ impl DecimalData { ) -> VortexResult { Self::validate(&values, values_type)?; - // SAFETY: validate ensures all invariants are met. + // SAFETY: `validate` checks the buffer layout. Values outside the declared precision can + // produce incorrect results, but do not affect memory safety. Ok(unsafe { Self::new_unchecked_handle(values, values_type, decimal_dtype) }) } @@ -297,10 +311,10 @@ impl DecimalData { } } - /// Validates the components that would be used to create a [`DecimalArray`] from a byte buffer. + /// Validates the layout of a decimal values buffer. /// - /// This function checks all the invariants required by [`DecimalArray::new_unchecked`]. - fn validate(buffer: &BufferHandle, values_type: DecimalType) -> VortexResult<()> { + /// Checks byte length and alignment for `values_type`. Does not read values or check validity. + pub(crate) fn validate(buffer: &BufferHandle, values_type: DecimalType) -> VortexResult<()> { let byte_width = values_type.byte_width(); vortex_ensure!( buffer.len().is_multiple_of(byte_width), diff --git a/vortex-array/src/arrays/decimal/vtable/mod.rs b/vortex-array/src/arrays/decimal/vtable/mod.rs index 99d7314bbfa..64691a6cb20 100644 --- a/vortex-array/src/arrays/decimal/vtable/mod.rs +++ b/vortex-array/src/arrays/decimal/vtable/mod.rs @@ -50,9 +50,13 @@ pub(crate) fn initialize(session: &VortexSession) { kernel::initialize(session); } -// The type of the values can be determined by looking at the type info...right? +/// Metadata for the existing `vortex.decimal` wire representation. #[derive(prost::Message)] pub struct DecimalMetadata { + /// Stored [`DecimalType`] discriminant, retained for wire compatibility. + /// + /// The legacy format records the actual width separately from the logical dtype. Readers + /// need this field even when the in-memory representation uses canonical decimal leaves. #[prost(int32, tag = "1")] pub(super) values_type: i32, } diff --git a/vortex-array/src/arrays/widen/compute/aggregate.rs b/vortex-array/src/arrays/widen/compute/aggregate.rs index 731505109c7..6552df57d0f 100644 --- a/vortex-array/src/arrays/widen/compute/aggregate.rs +++ b/vortex-array/src/arrays/widen/compute/aggregate.rs @@ -86,6 +86,9 @@ fn aggregate_decimal( return Ok(Some(partial)); } + // Kernel registrations apply to the whole encoding. Keep this decimal allowlist so adding an + // integer aggregate cannot enable it here without checking its partials against canonical decimal + // execution. Every other decimal aggregate uses the default path. if !(aggregate_fn.is::() || aggregate_fn.is::() || aggregate_fn.is::() diff --git a/vortex-array/src/arrays/widen/compute/fill_null.rs b/vortex-array/src/arrays/widen/compute/fill_null.rs index d0b36961911..14cc605e8b3 100644 --- a/vortex-array/src/arrays/widen/compute/fill_null.rs +++ b/vortex-array/src/arrays/widen/compute/fill_null.rs @@ -27,7 +27,7 @@ impl FillNullReduce for Widen { let values = array.values(); let dtype = match values.dtype() { DType::Primitive(..) => { - let Some(ptype) = ptype_holding_values_and_scalar(array, fill_value)? else { + let Some(ptype) = ptype_holding_values_and_scalar(array, fill_value) else { return Ok(None); }; diff --git a/vortex-array/src/arrays/widen/compute/mod.rs b/vortex-array/src/arrays/widen/compute/mod.rs index b930fdc90d1..fef7485c7a2 100644 --- a/vortex-array/src/arrays/widen/compute/mod.rs +++ b/vortex-array/src/arrays/widen/compute/mod.rs @@ -31,6 +31,7 @@ mod slice; mod take; mod zip; +use vortex_error::VortexExpect; use vortex_error::VortexResult; use crate::ArrayRef; @@ -40,7 +41,6 @@ use crate::arrays::ConstantArray; use crate::arrays::Widen; use crate::arrays::WidenArray; use crate::arrays::widen::WidenArraySlotsExt; -use crate::arrays::widen::encode::integer_value; use crate::arrays::widen::encode::smallest_integer_ptype; use crate::builtins::ArrayBuiltins; use crate::dtype::DType; @@ -69,29 +69,30 @@ pub(super) fn wrap_values(values: ArrayRef, dtype: DType) -> VortexResult, scalar: &Scalar, -) -> VortexResult> { +) -> Option { let (DType::Primitive(values_ptype, _), DType::Primitive(logical_ptype, _)) = (array.values().dtype(), array.dtype()) else { - return Ok(None); + return None; }; if !scalar.dtype().eq_ignore_nullability(array.dtype()) { - return Ok(None); + return None; } if scalar.is_null() { - return Ok(Some(*values_ptype)); + return Some(*values_ptype); } - let value = integer_value(scalar)?; - let Some(scalar_ptype) = smallest_integer_ptype(value..=value, logical_ptype.is_signed_int()) - else { - return Ok(None); - }; + let value = scalar + .as_primitive() + .pvalue() + .and_then(PValue::as_i128) + .vortex_expect("the non-null scalar has the Widen's integer dtype"); + let scalar_ptype = smallest_integer_ptype(value..=value, logical_ptype.is_signed_int())?; if scalar_ptype.byte_width() > values_ptype.byte_width() { - Ok(Some(scalar_ptype)) + Some(scalar_ptype) } else { - Ok(Some(*values_ptype)) + Some(*values_ptype) } } diff --git a/vortex-array/src/arrays/widen/compute/numeric.rs b/vortex-array/src/arrays/widen/compute/numeric.rs index 54f51c5d214..84046fbc855 100644 --- a/vortex-array/src/arrays/widen/compute/numeric.rs +++ b/vortex-array/src/arrays/widen/compute/numeric.rs @@ -25,7 +25,7 @@ use crate::arrays::scalar_fn::ScalarFnArrayView; use crate::arrays::widen::WidenArraySlotsExt; use crate::arrays::widen::compute::cast_keeping_nullability; use crate::arrays::widen::compute::wrap_values; -use crate::arrays::widen::encode::integer_value; +use crate::arrays::widen::encode::integer_extrema; use crate::arrays::widen::encode::smallest_integer_ptype; use crate::dtype::DType; use crate::dtype::PType; @@ -162,9 +162,7 @@ fn valid_value_interval( return Ok(None); }; - Ok(Some( - integer_value(&bounds.min)?..=integer_value(&bounds.max)?, - )) + Ok(Some(integer_extrema(bounds))) } /// Returns the smallest interval that holds `lhs operator rhs` for every value of `lhs` and every diff --git a/vortex-array/src/arrays/widen/compute/zip.rs b/vortex-array/src/arrays/widen/compute/zip.rs index 69f3f1282c5..ab5e6e692bb 100644 --- a/vortex-array/src/arrays/widen/compute/zip.rs +++ b/vortex-array/src/arrays/widen/compute/zip.rs @@ -30,7 +30,7 @@ impl ZipKernel for Widen { }; (other.values().clone(), ptype) } else if let Some(constant) = if_false.as_constant() { - let Some(ptype) = ptype_holding_values_and_scalar(array, constant)? else { + let Some(ptype) = ptype_holding_values_and_scalar(array, constant) else { return Ok(None); }; (if_false.clone(), ptype) diff --git a/vortex-array/src/arrays/widen/encode.rs b/vortex-array/src/arrays/widen/encode.rs deleted file mode 100644 index 141fccde6d2..00000000000 --- a/vortex-array/src/arrays/widen/encode.rs +++ /dev/null @@ -1,206 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -//! Selection of narrower `Widen` children from valid values. -//! -//! Null payloads do not constrain the child dtype. The result keeps the logical dtype and uses -//! narrower storage only when that storage can hold every valid value. - -use std::ops::RangeInclusive; - -use vortex_error::VortexResult; -use vortex_error::vortex_bail; -use vortex_error::vortex_ensure; - -use crate::ArrayRef; -use crate::ExecutionCtx; -use crate::IntoArray; -use crate::aggregate_fn::NumericalAggregateOpts; -use crate::aggregate_fn::fns::min_max::min_max; -use crate::arrays::DecimalArray; -use crate::arrays::PrimitiveArray; -use crate::arrays::WidenArray; -use crate::arrays::decimal::DecimalArrayExt; -use crate::arrays::decimal::copy_to_canonical_width; -use crate::arrays::decimal::valid_range; -use crate::arrays::widen::compute::wrap_values; -use crate::builtins::ArrayBuiltins; -use crate::dtype::DType; -use crate::dtype::DecimalDType; -use crate::dtype::NativeDecimalType; -use crate::dtype::PType; -use crate::dtype::i256; -use crate::match_each_decimal_value_type; -use crate::scalar::DecimalValue; -use crate::scalar::Scalar; - -impl WidenArray { - /// Stores `array` as a `Widen` over the smallest integer type with the same signedness that - /// holds its valid values, or returns `array` unchanged when no narrower type holds them. - /// - /// Null rows do not count, so their payloads can be outside the stored type, and an empty or - /// all-null array gets a 1-byte type. Fails when `array` is not an integer array. - pub fn encode(array: PrimitiveArray, ctx: &mut ExecutionCtx) -> VortexResult { - let dtype = array.dtype().clone(); - wrap_values(narrow_values(array, ctx)?.into_array(), dtype) - } - - /// Stores a decimal array at the smallest precision that its valid values and scale allow. - /// - /// The result keeps the input dtype and nullability. Empty and all-null arrays use the - /// precision required by the scale, or 1 for a non-positive scale. Returns the input unchanged - /// when the selected precision has the canonical width of the input dtype. - /// - /// # Errors - /// - /// Returns an error if a valid value does not fit the input precision. - pub fn encode_decimal(array: DecimalArray, ctx: &mut ExecutionCtx) -> VortexResult { - let logical_decimal_dtype = array.decimal_dtype(); - let validity = array.validity()?; - let validity_mask = validity.execute_mask(array.len(), ctx)?; - let valid_value_range = match_each_decimal_value_type!(array.values_type(), |T| { - valid_range(array.buffer::().as_slice(), &validity_mask).map(|(min, max)| { - DecimalValue::from(min).as_i256()..=DecimalValue::from(max).as_i256() - }) - }); - - let child_decimal_dtype = smallest_decimal_dtype(valid_value_range, logical_decimal_dtype)?; - if child_decimal_dtype.canonical_values_type() - == logical_decimal_dtype.canonical_values_type() - { - return Ok(array.into_array()); - } - - let child_buffer_handle = match_each_decimal_value_type!(array.values_type(), |T| { - copy_to_canonical_width( - array.buffer::().as_slice(), - &validity_mask, - child_decimal_dtype, - )? - }); - let logical_dtype = array.dtype().clone(); - - let child = DecimalArray::new_handle( - child_buffer_handle, - child_decimal_dtype.canonical_values_type(), - child_decimal_dtype, - validity, - ); - - Ok(WidenArray::try_new(child.into_array(), logical_dtype)?.into_array()) - } - - /// Returns `array` cast to the type that [`Self::encode`] chooses for its values, without a - /// `Widen` around them. - /// - /// The result keeps the signedness and the validity of `array`, but its dtype can be narrower. - /// A float array returns unchanged, where [`Self::encode`] fails. This is for an internal child - /// whose parent chooses its dtype, such as list offsets or dictionary codes. An array whose - /// dtype is fixed, such as a column, needs [`Self::encode`] instead. - #[deprecated( - note = "Changes the dtype. Each caller must decide whether it needs a Widen (#10733)" - )] - pub fn encode_values( - array: PrimitiveArray, - ctx: &mut ExecutionCtx, - ) -> VortexResult { - if !array.ptype().is_int() { - return Ok(array); - } - - narrow_values(array, ctx) - } -} - -/// Returns `array` cast to the stored type that [`WidenArray::encode`] chooses, or `array` -/// unchanged when no narrower type holds its valid values. -/// -/// [`WidenArray::encode`] and the deprecated [`WidenArray::encode_values`] share this function, -/// so that `encode` needs no `#[expect(deprecated)]`. Fails when `array` is not an integer array. -fn narrow_values(array: PrimitiveArray, ctx: &mut ExecutionCtx) -> VortexResult { - let ptype = array.ptype(); - vortex_ensure!(ptype.is_int(), "Widen requires integer values, got {ptype}"); - - // No integer type is narrower than one byte, so the bounds cannot change the result. - if ptype.byte_width() == 1 { - return Ok(array); - } - - // An array without valid values fits any type, and the bounds `0..=0` select the 1-byte type. - let bounds = match min_max(array.as_ref(), ctx, NumericalAggregateOpts::default())? { - Some(bounds) => integer_value(&bounds.min)?..=integer_value(&bounds.max)?, - None => 0..=0, - }; - let Some(stored_ptype) = smallest_integer_ptype(bounds, ptype.is_signed_int()) - .filter(|stored_ptype| stored_ptype.byte_width() < ptype.byte_width()) - else { - return Ok(array); - }; - - let stored_dtype = DType::Primitive(stored_ptype, array.dtype().nullability()); - array - .into_array() - .cast(stored_dtype)? - .execute::(ctx) -} - -/// Returns the smallest integer type with the given signedness that holds every value in `bounds`, -/// or `None` when no integer type of at most 64 bits holds them. -pub(super) fn smallest_integer_ptype(bounds: RangeInclusive, signed: bool) -> Option { - let (min, max) = bounds.into_inner(); - if signed { - let min_ptype = PType::min_signed_ptype_for_value(i64::try_from(min).ok()?); - let max_ptype = PType::min_signed_ptype_for_value(i64::try_from(max).ok()?); - Some(min_ptype.max_signed_ptype(max_ptype)) - } else { - let min_ptype = PType::min_unsigned_ptype_for_value(u64::try_from(min).ok()?); - let max_ptype = PType::min_unsigned_ptype_for_value(u64::try_from(max).ok()?); - Some(min_ptype.max_unsigned_ptype(max_ptype)) - } -} - -/// Returns the value of a non-null integer scalar of at most 64 bits. -pub(super) fn integer_value(scalar: &Scalar) -> VortexResult { - if scalar.dtype().is_signed_int() { - i64::try_from(scalar).map(i128::from) - } else { - u64::try_from(scalar).map(i128::from) - } -} - -/// Returns the smallest decimal dtype that holds an unscaled valid-value range at the parent scale. -/// -/// The precision is at least 1 and at least a positive scale. An absent range represents an empty -/// or all-null array and needs only that minimum precision. The result never exceeds the parent -/// precision. Callers **must** exclude null payloads from the range, for example with -/// [`valid_range`]. -/// -/// # Errors -/// -/// Returns an error if the minimum exceeds the maximum or an endpoint exceeds the parent precision. -pub(crate) fn smallest_decimal_dtype( - range: Option>, - parent: DecimalDType, -) -> VortexResult { - let minimum_precision = parent.scale().max(1) as u8; - let Some(range) = range else { - // Empty and all-null arrays have no valid values to constrain precision. The scale still - // requires at least that many digits when positive, or precision 1 when non-positive. - return Ok(DecimalDType::new(minimum_precision, parent.scale())); - }; - - let (min, max) = range.into_inner(); - vortex_ensure!( - min <= max, - "Decimal range requires ordered extrema, got {min} to {max}" - ); - - for precision in minimum_precision..=parent.precision() { - let index = usize::from(precision); - if i256::MIN_BY_PRECISION[index] <= min && max <= i256::MAX_BY_PRECISION[index] { - return Ok(DecimalDType::new(precision, parent.scale())); - } - } - - vortex_bail!("Decimal range must fit {parent}, got {min} to {max}") -} diff --git a/vortex-array/src/arrays/widen/encode/decimal.rs b/vortex-array/src/arrays/widen/encode/decimal.rs new file mode 100644 index 00000000000..dc7ed5e0db6 --- /dev/null +++ b/vortex-array/src/arrays/widen/encode/decimal.rs @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Decimal encoding and conversion of legacy storage into canonical decimal leaves. +//! +//! The legacy `vortex.decimal` format stores integer width independently of logical precision. +//! DecimalByteParts assembly can also produce such buffers, but we now require canonical +//! `vortex.decimal` leaves to have a width determined and fixed by their precision. +//! +//! In this new world, we can retain narrow storage as a [`WidenArray`] over a canonical child. +//! Current [`WidenPlugin`] writes still use the legacy format, so this conversion can also apply +//! to newly written data (which will happen until `Widen` is added to the `core` edition). +//! +//! We unify child selection in both encoding and legacy conversion because it has non-trivial +//! logic: width alone does not determine a valid child dtype. For example, unscaled 127 fits `i8` +//! but needs precision 3, whose canonical width is `i16`. +//! +//! [`WidenPlugin`]: crate::arrays::widen::WidenPlugin + +use std::ops::RangeInclusive; + +use vortex_buffer::Buffer; +use vortex_error::VortexExpect; +use vortex_error::VortexResult; +use vortex_error::vortex_bail; +use vortex_error::vortex_ensure; +use vortex_error::vortex_ensure_eq; +use vortex_mask::Mask; + +use crate::ArrayRef; +use crate::ExecutionCtx; +use crate::IntoArray; +use crate::arrays::DecimalArray; +use crate::arrays::WidenArray; +use crate::arrays::decimal::DecimalArrayExt; +use crate::arrays::decimal::DecimalData; +use crate::arrays::decimal::DecimalDataParts; +use crate::arrays::decimal::copy_to_canonical_width; +use crate::arrays::decimal::valid_range; +use crate::buffer::BufferHandle; +use crate::dtype::DType; +use crate::dtype::DecimalDType; +use crate::dtype::NativeDecimalType; +use crate::dtype::i256; +use crate::expr::stats::Precision; +use crate::expr::stats::Stat; +use crate::match_each_decimal_value_type; +use crate::scalar::DecimalValue; +use crate::scalar::ScalarValue; +use crate::validity::Validity; + +impl WidenArray { + /// Stores a decimal array at the smallest precision that its valid values and scale allow. + /// + /// We keep the logical dtype and nullability by wrapping the narrower child in a + /// [`WidenArray`]. Reducing precision within the same storage width saves no space, so in that + /// case we return the input unchanged. + /// + /// Empty and all-null arrays have no values to constrain precision, but we still need to keep + /// the scale. They use precision 1 or the scale, whichever is greater. If a child is selected, + /// we cache any exact extrema as [`Stat::Min`] and [`Stat::Max`] for later aggregates. + /// + /// # Errors + /// + /// Returns an error if a valid value does not fit the input precision. + pub fn encode_decimal(array: DecimalArray, ctx: &mut ExecutionCtx) -> VortexResult { + let logical_decimal_dtype = array.decimal_dtype(); + let validity = array.validity()?; + let validity_mask = validity.execute_mask(array.len(), ctx)?; + let child = match_each_decimal_value_type!(array.values_type(), |T| { + let values_buffer = array.buffer::(); + let valid_value_range = + ValidRange::resolve(Precision::Absent, values_buffer.as_slice(), &validity_mask); + narrowest_decimal_child( + &values_buffer, + &validity, + &validity_mask, + valid_value_range, + logical_decimal_dtype, + )? + }); + + let Some(child) = child else { + return Ok(array.into_array()); + }; + + Ok(WidenArray::try_new(child.into_array(), array.dtype().clone())?.into_array()) + } + + /// Converts stored decimals into a canonical leaf or a [`WidenArray`] over one. + /// + /// Legacy decimal readers and DecimalByteParts assembly can give us a buffer whose width does + /// not match the logical precision. We need a canonical leaf without changing the logical + /// dtype or widening narrow storage unnecessarily. + /// + /// If the width is already canonical, we trust the declared precision and reuse the buffer + /// without reading its values. This also lets device buffers stay on the device. Otherwise, + /// child selection follows [`Self::encode_decimal`], keeping the logical dtype and validity. + /// We reuse buffers at the selected width and convert other widths without rescaling. + /// A narrowing copy writes zeros in null slots. A reused buffer keeps its null slots. + /// + /// Scanning and conversion use CPU kernels, so noncanonical buffers are brought to the host. + /// Their validity **must** already be executable on the host because these kernels need a mask + /// to exclude null payloads. + /// + /// Readers and decoders may already know the range. For noncanonical storage, `known_range` + /// lets us reuse it: + /// + /// - [`Precision::Absent`] scans valid values to find their extrema. + /// - [`Precision::Exact`] **must** give the actual minimum and maximum valid unscaled values. + /// - [`Precision::Inexact`] **must** contain every valid unscaled value. + /// + /// We cache the range on the child as [`Stat::Min`] and [`Stat::Max`], keeping its exactness + /// because enclosing bounds are not necessarily actual extrema. Scanning empty or all-null + /// input yields no extrema to cache. Canonical-width storage ignores `known_range`. + /// Supplied ranges are trusted, even when a narrowing copy checks the selected precision. + /// Incorrect ranges or declared precision can cause incorrect results in later operations. + /// + /// # Errors + /// + /// Rejects malformed buffer layout or validity length. For noncanonical storage, also rejects + /// failed host copies, reversed ranges, and range bounds outside the logical precision. A + /// narrowing copy rejects valid values outside the selected precision. + pub fn from_stored_decimal( + parts: DecimalDataParts, + known_range: Precision>, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + let DecimalDataParts { + decimal_dtype, + values, + values_type, + validity, + } = parts; + + // Validate before typed buffer access or mask execution, which can panic on malformed data. + DecimalData::validate(&values, values_type)?; + let len = values.len() / values_type.byte_width(); + if let Some(validity_len) = validity.maybe_len() { + vortex_ensure_eq!( + validity_len, + len, + "Decimal validity must have one entry per value" + ); + } + + if values_type == decimal_dtype.canonical_values_type() { + let array = DecimalArray::try_new_handle(values, values_type, decimal_dtype, validity)?; + + return Ok(array.into_array()); + } + + let values = values.try_into_host_sync()?; + let mask = validity.execute_mask(len, ctx)?; + match_each_decimal_value_type!(values_type, |T| { + let values = Buffer::::from_byte_buffer(values); + let range = ValidRange::resolve(known_range, values.as_slice(), &mask); + widen_or_canonical(values, validity, &mask, range, decimal_dtype) + }) + } +} + +/// Chooses child precision from a valid-value range and the parent scale. +/// +/// We keep the parent scale so values do not need rescaling. The child precision must therefore +/// be at least 1 and at least the scale. With empty or all-null input, these are the only +/// constraints. Pass `None` for that case. Otherwise, null payloads **must** be excluded from the +/// range. +/// +/// # Errors +/// +/// Returns an error if the minimum exceeds the maximum or an endpoint exceeds the parent precision. +pub(crate) fn smallest_decimal_dtype( + range: Option>, + parent: DecimalDType, +) -> VortexResult { + let minimum_precision = parent.scale().max(1) as u8; + let Some(range) = range else { + return Ok(DecimalDType::new(minimum_precision, parent.scale())); + }; + + let (min, max) = range.into_inner(); + vortex_ensure!( + min <= max, + "Decimal range requires ordered extrema, got {min} to {max}" + ); + + for precision in minimum_precision..=parent.precision() { + let index = usize::from(precision); + if i256::MIN_BY_PRECISION[index] <= min && max <= i256::MAX_BY_PRECISION[index] { + return Ok(DecimalDType::new(precision, parent.scale())); + } + } + + vortex_bail!("Decimal range must fit {parent}, got {min} to {max}") +} + +/// Value bounds for choosing child precision and caching statistics. +/// +/// Supplied bounds need not be actual extrema, so we keep their exactness when caching them. +/// `Empty` means a scan found no valid values, rather than that statistics are unavailable. +enum ValidRange { + /// No value is valid. + Empty, + /// The smallest and the largest valid value. + Exact(RangeInclusive), + /// Bounds that hold every valid value. They need not be valid values themselves. + Bounds(RangeInclusive), +} + +impl ValidRange { + /// Uses known bounds when available, otherwise scans valid values. + /// + /// Readers and decoders may already have bounds from their input statistics. Reusing them + /// avoids scanning the buffer again. + fn resolve( + known_range: Precision>, + values: &[F], + mask: &Mask, + ) -> Self { + match known_range { + Precision::Exact(extrema) => return Self::Exact(extrema), + Precision::Inexact(bounds) => return Self::Bounds(bounds), + Precision::Absent => {} + } + + let Some((min, max)) = valid_range(values, mask) else { + return Self::Empty; + }; + let min = min.to_i256().vortex_expect("every decimal value fits i256"); + let max = max.to_i256().vortex_expect("every decimal value fits i256"); + + Self::Exact(min..=max) + } + + /// Returns the bounds that hold every valid value, or `None` when no value is valid. + fn bounds(&self) -> Option> { + match self { + Self::Empty => None, + Self::Exact(bounds) | Self::Bounds(bounds) => Some(bounds.clone()), + } + } + + /// Caches bounds on the child so later aggregates can reuse them without another scan. + /// + /// The bounds **must** fit the child's selected precision. + fn cache_statistics(self, child: &DecimalArray) { + let extrema = match self { + Self::Empty => return, + Self::Exact(bounds) => Precision::Exact(bounds), + Self::Bounds(bounds) => Precision::Inexact(bounds), + }; + let child_dtype = child.decimal_dtype(); + let min = extrema + .clone() + .map(|bounds| decimal_stat(*bounds.start(), child_dtype)); + let max = extrema.map(|bounds| decimal_stat(*bounds.end(), child_dtype)); + child.statistics().set(Stat::Min, min); + child.statistics().set(Stat::Max, max); + } +} + +/// Wraps a narrower canonical child, or builds a leaf at the logical width. +/// +/// The result must keep the logical dtype. Returning a narrow child directly would change it. +/// +/// `mask` **must** be the mask of `validity`. +fn widen_or_canonical( + values: Buffer, + validity: Validity, + mask: &Mask, + range: ValidRange, + decimal_dtype: DecimalDType, +) -> VortexResult { + if let Some(child) = narrowest_decimal_child(&values, &validity, mask, range, decimal_dtype)? { + let dtype = DType::Decimal(decimal_dtype, validity.nullability()); + return Ok(WidenArray::try_new(child.into_array(), dtype)?.into_array()); + } + + let array = build_canonical_decimal(&values, &validity, mask, decimal_dtype)?; + + Ok(array.into_array()) +} + +/// Builds the narrowest canonical child, or returns `None` if no narrower width exists. +/// +/// Reducing precision without changing width saves no space, so we return `None` in that case. +/// +/// Every valid value **must** lie in `range`, and `mask` **must** be the mask of `validity`. +fn narrowest_decimal_child( + values: &Buffer, + validity: &Validity, + mask: &Mask, + range: ValidRange, + decimal_dtype: DecimalDType, +) -> VortexResult> { + let child_dtype = smallest_decimal_dtype(range.bounds(), decimal_dtype)?; + let child_type = child_dtype.canonical_values_type(); + if child_type == decimal_dtype.canonical_values_type() { + return Ok(None); + } + + let child = build_canonical_decimal(values, validity, mask, child_dtype)?; + range.cache_statistics(&child); + + Ok(Some(child)) +} + +/// Reuses storage at the dtype's canonical width, copying only when conversion is needed. +/// +/// Every valid value **must** fit `decimal_dtype`, and `mask` **must** describe `validity`. +fn build_canonical_decimal( + values: &Buffer, + validity: &Validity, + mask: &Mask, + decimal_dtype: DecimalDType, +) -> VortexResult { + let values_type = decimal_dtype.canonical_values_type(); + let buffer = if F::DECIMAL_TYPE == values_type { + // The copy helper allocates even at the same width, so retain the existing buffer here. + BufferHandle::new_host(values.clone().into_byte_buffer()) + } else { + copy_to_canonical_width(values.as_slice(), mask, decimal_dtype)? + }; + + DecimalArray::try_new_handle(buffer, values_type, decimal_dtype, validity.clone()) +} + +/// Represents a cached bound using the child dtype. +/// +/// These statistics belong to the child, so their values need its canonical width rather than +/// the logical parent's width. `value` **must** fit the child's precision. +fn decimal_stat(value: i256, decimal_dtype: DecimalDType) -> ScalarValue { + let value = DecimalValue::try_from_i256(value, decimal_dtype) + .vortex_expect("the selected child precision contains its range bounds"); + + ScalarValue::Decimal(value) +} diff --git a/vortex-array/src/arrays/widen/encode/integer.rs b/vortex-array/src/arrays/widen/encode/integer.rs new file mode 100644 index 00000000000..f9758562f1b --- /dev/null +++ b/vortex-array/src/arrays/widen/encode/integer.rs @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Integer narrowing that preserves signedness and excludes null payloads. +//! +//! Encoding and the deprecated dtype-changing helper share this implementation so they select the +//! same storage type for each valid-value range. + +use std::ops::RangeInclusive; + +use vortex_error::VortexExpect; +use vortex_error::VortexResult; +use vortex_error::vortex_ensure; + +use crate::ArrayRef; +use crate::ExecutionCtx; +use crate::IntoArray; +use crate::aggregate_fn::NumericalAggregateOpts; +use crate::aggregate_fn::fns::min_max::MinMaxResult; +use crate::aggregate_fn::fns::min_max::min_max; +use crate::arrays::PrimitiveArray; +use crate::arrays::WidenArray; +use crate::arrays::widen::compute::wrap_values; +use crate::builtins::ArrayBuiltins; +use crate::dtype::DType; +use crate::dtype::PType; +use crate::scalar::PValue; + +impl WidenArray { + /// Stores `array` as a `Widen` over the smallest integer type with the same signedness that + /// holds its valid values, or returns `array` unchanged when no narrower type holds them. + /// + /// Null rows do not count, so their payloads can be outside the stored type, and an empty or + /// all-null array gets a 1-byte type. Fails when `array` is not an integer array. + pub fn encode(array: PrimitiveArray, ctx: &mut ExecutionCtx) -> VortexResult { + let dtype = array.dtype().clone(); + wrap_values(narrow_values(array, ctx)?.into_array(), dtype) + } + + /// Returns `array` cast to the type that [`Self::encode`] chooses for its values, without a + /// `Widen` around them. + /// + /// The result keeps the signedness and the validity of `array`, but its dtype can be narrower. + /// A float array returns unchanged, where [`Self::encode`] fails. This is for an internal child + /// whose parent chooses its dtype, such as list offsets or dictionary codes. An array whose + /// dtype is fixed, such as a column, needs [`Self::encode`] instead. + #[deprecated( + note = "Changes the dtype. Each caller must decide whether it needs a Widen (#10733)" + )] + pub fn encode_values( + array: PrimitiveArray, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + if !array.ptype().is_int() { + return Ok(array); + } + + narrow_values(array, ctx) + } +} + +/// Returns `array` cast to the stored type that [`WidenArray::encode`] chooses, or `array` +/// unchanged when no narrower type holds its valid values. +/// +/// [`WidenArray::encode`] and the deprecated [`WidenArray::encode_values`] share this function, +/// so that `encode` needs no `#[expect(deprecated)]`. Fails when `array` is not an integer array. +fn narrow_values(array: PrimitiveArray, ctx: &mut ExecutionCtx) -> VortexResult { + let ptype = array.ptype(); + vortex_ensure!(ptype.is_int(), "Widen requires integer values, got {ptype}"); + + // No integer type is narrower than one byte, so the bounds cannot change the result. + if ptype.byte_width() == 1 { + return Ok(array); + } + + // An array without valid values fits any type, and the bounds `0..=0` select the 1-byte type. + let bounds = match min_max(array.as_ref(), ctx, NumericalAggregateOpts::default())? { + Some(bounds) => integer_extrema(bounds), + None => 0..=0, + }; + let Some(stored_ptype) = smallest_integer_ptype(bounds, ptype.is_signed_int()) + .filter(|stored_ptype| stored_ptype.byte_width() < ptype.byte_width()) + else { + return Ok(array); + }; + + let stored_dtype = DType::Primitive(stored_ptype, array.dtype().nullability()); + array + .into_array() + .cast(stored_dtype)? + .execute::(ctx) +} + +/// Widens integer extrema to `i128`, which holds every integer [`PValue`] exactly. +/// +/// Both extrema **must** be non-null integers, as returned by [`min_max`] on an integer array. +pub(in crate::arrays::widen) fn integer_extrema(bounds: MinMaxResult) -> RangeInclusive { + let [min, max] = [bounds.min, bounds.max].map(|bound| { + bound + .as_primitive() + .pvalue() + .and_then(PValue::as_i128) + .vortex_expect("min_max of an integer array returns non-null integer extrema") + }); + + min..=max +} + +/// Returns the smallest integer type with the given signedness that holds every value in `bounds`, +/// or `None` when no integer type of at most 64 bits holds them. +pub(in crate::arrays::widen) fn smallest_integer_ptype( + bounds: RangeInclusive, + signed: bool, +) -> Option { + let (min, max) = bounds.into_inner(); + if signed { + let min_ptype = PType::min_signed_ptype_for_value(i64::try_from(min).ok()?); + let max_ptype = PType::min_signed_ptype_for_value(i64::try_from(max).ok()?); + Some(min_ptype.max_signed_ptype(max_ptype)) + } else { + let min_ptype = PType::min_unsigned_ptype_for_value(u64::try_from(min).ok()?); + let max_ptype = PType::min_unsigned_ptype_for_value(u64::try_from(max).ok()?); + Some(min_ptype.max_unsigned_ptype(max_ptype)) + } +} diff --git a/vortex-array/src/arrays/widen/encode/mod.rs b/vortex-array/src/arrays/widen/encode/mod.rs new file mode 100644 index 00000000000..da2219d8044 --- /dev/null +++ b/vortex-array/src/arrays/widen/encode/mod.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Selection of narrower `Widen` children from valid values. +//! +//! Integer narrowing and decimal conversion use separate implementations. Decimal encoding and +//! stored-value reads share one child-selection path so they cannot disagree about its precision. + +mod decimal; +#[cfg(test)] +pub(crate) use decimal::smallest_decimal_dtype; + +mod integer; +pub(super) use integer::integer_extrema; +pub(super) use integer::smallest_integer_ptype; diff --git a/vortex-array/src/arrays/widen/mod.rs b/vortex-array/src/arrays/widen/mod.rs index f387802df0b..72ba52b208a 100644 --- a/vortex-array/src/arrays/widen/mod.rs +++ b/vortex-array/src/arrays/widen/mod.rs @@ -12,8 +12,9 @@ //! //! A decimal child has the logical scale and a lower precision, and **must** be valid for its dtype. //! Comparisons, bounds checks and fills use the child dtype to bound its valid values. -//! [`WidenArray::encode_decimal`] picks the smallest precision that holds the valid values and the -//! scale. Decimal execution copies the child values into the logical canonical width. +//! [`WidenArray::encode_decimal`] and [`WidenArray::from_stored_decimal`] choose child precision +//! from valid values or supplied bounds, at the logical scale. Decimal execution copies the child +//! values into the logical canonical width. //! //! Narrowing is a physical choice, so it **must not** change a result. Execution casts the values //! to the logical dtype for integers, and an operation runs on the values only where it gives the diff --git a/vortex-array/src/arrays/widen/tests/decimal_stored.rs b/vortex-array/src/arrays/widen/tests/decimal_stored.rs new file mode 100644 index 00000000000..efc37b5b600 --- /dev/null +++ b/vortex-array/src/arrays/widen/tests/decimal_stored.rs @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Stored decimal conversion, buffer reuse, and range statistics. +//! +//! Compares converted values with canonical arrays and checks the selected child representation. + +use std::ops::RangeInclusive; + +use rstest::rstest; +use vortex_buffer::Buffer; +use vortex_buffer::ByteBuffer; +use vortex_error::VortexExpect; +use vortex_error::VortexResult; + +use super::SESSION; +use crate::ArrayRef; +use crate::IntoArray; +use crate::VortexSessionExecute; +use crate::arrays::BoolArray; +use crate::arrays::Decimal; +use crate::arrays::DecimalArray; +use crate::arrays::Widen; +use crate::arrays::WidenArray; +use crate::arrays::decimal::DecimalArrayExt; +use crate::arrays::decimal::DecimalDataParts; +use crate::arrays::widen::WidenArraySlotsExt; +use crate::assert_arrays_eq; +use crate::buffer::BufferHandle; +use crate::dtype::BigCast; +use crate::dtype::DecimalDType; +use crate::dtype::DecimalType; +use crate::dtype::i256; +use crate::expr::stats::Precision; +use crate::expr::stats::Stat; +use crate::expr::stats::StatsProvider; +use crate::match_each_decimal_value_type; +use crate::scalar::DecimalValue; +use crate::scalar::Scalar; +use crate::validity::Validity; + +/// Returns `values` as a buffer of `values_type`, with zero in each null slot. +fn stored_buffer(values: &[Option], values_type: DecimalType) -> ByteBuffer { + match_each_decimal_value_type!(values_type, |T| { + values + .iter() + .map(|value| { + ::from(value.unwrap_or_default()) + .vortex_expect("fixture values fit the stored width") + }) + .collect::>() + .into_byte_buffer() + }) +} + +/// Uses non-nullable validity when every value is present. +fn validity_of(values: &[Option]) -> Validity { + if values.iter().all(Option::is_some) { + return Validity::NonNullable; + } + + Validity::from_iter(values.iter().map(Option::is_some)) +} + +/// Returns the parts of decimal `values` stored as `values_type`. +fn stored_parts( + values: &[Option], + values_type: DecimalType, + decimal_dtype: DecimalDType, +) -> DecimalDataParts { + DecimalDataParts { + decimal_dtype, + values: BufferHandle::new_host(stored_buffer(values, values_type)), + values_type, + validity: validity_of(values), + } +} + +/// Returns `values` as the canonical array of `decimal_dtype`. +fn canonical_reference(values: &[Option], decimal_dtype: DecimalDType) -> DecimalArray { + let values_type = decimal_dtype.canonical_values_type(); + DecimalArray::new_handle( + BufferHandle::new_host(stored_buffer(values, values_type)), + values_type, + decimal_dtype, + validity_of(values), + ) +} + +/// Returns the decimal storage, through a [`Widen`] if present. +fn decimal_leaf(array: &ArrayRef) -> DecimalArray { + let leaf = match array.as_opt::() { + Some(widen) => widen.values().clone(), + None => array.clone(), + }; + + leaf.downcast::() +} + +fn i256_range(min: i128, max: i128) -> RangeInclusive { + i256::from_i128(min)..=i256::from_i128(max) +} + +/// Returns two `i32` zeros, with the alignment of `i32`. +fn two_i32_values() -> BufferHandle { + BufferHandle::new_host(Buffer::::zeroed(2).into_byte_buffer()) +} + +/// Returns a validity array of `valid`. `Validity::from_iter` would turn an all-true input into +/// `Validity::AllValid`, which has no length to check. +fn validity_array(valid: &[bool]) -> Validity { + Validity::Array(BoolArray::from_iter(valid.iter().copied()).into_array()) +} + +#[rstest] +#[case::canonical( + vec![Some(-999_999_999), None, Some(999_999_999)], + DecimalType::I32, DecimalDType::new(9, 2), None, true, +)] +#[case::stored_as_child( + vec![Some(-9_999), Some(9_999)], + DecimalType::I16, DecimalDType::new(18, 2), Some(4), true, +)] +#[case::narrowing_copy( + vec![Some(-99), None, Some(99)], + DecimalType::I32, DecimalDType::new(18, 2), Some(2), false, +)] +#[case::widening_copy( + vec![Some(32_767)], + DecimalType::I16, DecimalDType::new(10, 2), Some(5), false, +)] +#[case::no_narrower_child( + vec![Some(1_000_000_000)], + DecimalType::I32, DecimalDType::new(18, 2), None, false, +)] +#[case::scale_bound( + vec![Some(1)], + DecimalType::I8, DecimalDType::new(18, 10), None, false, +)] +#[case::negative_boundary( + vec![Some(-128)], + DecimalType::I8, DecimalDType::new(10, 0), Some(3), false, +)] +#[case::i128_narrowing_copy( + vec![Some(-99), Some(99)], + DecimalType::I128, DecimalDType::new(18, 2), Some(2), false, +)] +#[case::i128_canonical_copy( + vec![Some(100_000_000_000_000_000)], + DecimalType::I128, DecimalDType::new(18, 2), None, false, +)] +#[case::i128_stored_as_child( + vec![Some(10i128.pow(37))], + DecimalType::I128, DecimalDType::new(40, 2), Some(38), true, +)] +#[case::i128_max_widens_to_i256( + vec![Some(i128::MAX)], + DecimalType::I128, DecimalDType::new(40, 0), None, false, +)] +#[case::i256_narrowing_copy( + vec![Some(-99), None, Some(99)], + DecimalType::I256, DecimalDType::new(18, 2), Some(2), false, +)] +#[case::i256_canonical_copy( + vec![Some(10i128.pow(37))], + DecimalType::I256, DecimalDType::new(38, 0), None, false, +)] +#[case::all_null( + vec![None, None], + DecimalType::I32, DecimalDType::new(18, 2), Some(2), false, +)] +#[case::empty(vec![], DecimalType::I32, DecimalDType::new(18, 2), Some(2), false)] +fn test_from_stored_decimal( + #[case] values: Vec>, + #[case] values_type: DecimalType, + #[case] decimal_dtype: DecimalDType, + #[case] child_precision: Option, + #[case] must_reuse_buffer: bool, +) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let parts = stored_parts(&values, values_type, decimal_dtype); + let input_ptr = parts.values.as_host().as_ptr(); + + let result = WidenArray::from_stored_decimal(parts, Precision::Absent, &mut ctx)?; + + assert_eq!(result.is::(), child_precision.is_some()); + let leaf = decimal_leaf(&result); + let leaf_dtype = leaf.decimal_dtype(); + let expected_leaf_dtype = match child_precision { + Some(precision) => DecimalDType::new(precision, decimal_dtype.scale()), + None => decimal_dtype, + }; + assert_eq!(leaf_dtype, expected_leaf_dtype); + assert_eq!(leaf.values_type(), leaf_dtype.canonical_values_type()); + if must_reuse_buffer { + assert_eq!(leaf.buffer_handle().as_host().as_ptr(), input_ptr); + } + assert_arrays_eq!( + result, + canonical_reference(&values, decimal_dtype), + &mut ctx + ); + + Ok(()) +} + +/// Canonical storage is reused without checking values against the declared precision. +#[test] +fn test_from_stored_decimal_trusts_canonical_width() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let parts = stored_parts(&[Some(100)], DecimalType::I8, DecimalDType::new(2, 0)); + let input_ptr = parts.values.as_host().as_ptr(); + + let result = WidenArray::from_stored_decimal(parts, Precision::Absent, &mut ctx)?; + + assert!(result.is::()); + let decimal = result.as_::(); + assert_eq!(decimal.buffer_handle().as_host().as_ptr(), input_ptr); + assert_eq!(decimal.buffer::().as_slice(), [100]); + + Ok(()) +} + +#[rstest] +#[case::value_outside_precision( + vec![Some(1), Some(100)], Precision::Absent, "Decimal range must fit", +)] +#[case::reversed_range( + vec![Some(1), Some(2)], Precision::Inexact(i256_range(2, 1)), "ordered extrema", +)] +#[case::range_outside_precision( + vec![Some(1), Some(2)], Precision::Inexact(i256_range(1, 100)), "Decimal range must fit", +)] +fn test_from_stored_decimal_rejects_invalid_range( + #[case] values: Vec>, + #[case] range: Precision>, + #[case] expected: &str, +) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let parts = stored_parts(&values, DecimalType::I16, DecimalDType::new(2, 0)); + + let error = WidenArray::from_stored_decimal(parts, range, &mut ctx) + .expect_err("an invalid value range must fail"); + assert!(error.to_string().contains(expected), "{error}"); + + Ok(()) +} + +#[test] +fn test_from_stored_decimal_ignores_null_slots() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let decimal_dtype = DecimalDType::new(9, 0); + let validity = Validity::from_iter([false, true, false]); + let stored = Buffer::copy_from([i128::MIN, 5, i128::MAX]); + let parts = DecimalDataParts { + decimal_dtype, + values: BufferHandle::new_host(stored.into_byte_buffer()), + values_type: DecimalType::I128, + validity, + }; + + let result = WidenArray::from_stored_decimal(parts, Precision::Absent, &mut ctx)?; + + let child = decimal_leaf(&result); + assert_eq!(child.decimal_dtype(), DecimalDType::new(1, 0)); + assert_eq!(child.buffer::().as_slice(), [0, 5, 0]); + assert_arrays_eq!( + result, + canonical_reference(&[None, Some(5), None], decimal_dtype), + &mut ctx + ); + + Ok(()) +} + +/// Supplied ranges retain their exactness. An absent range records scanned extrema. +#[rstest] +#[case::scanned( + vec![Some(-5), None, Some(7)], DecimalType::I128, Precision::Absent, + 2, Precision::Exact((-5, 7)), +)] +#[case::exact( + vec![Some(-5), None, Some(7)], DecimalType::I32, Precision::Exact(i256_range(-5, 7)), + 2, Precision::Exact((-5, 7)), +)] +#[case::inexact( + vec![Some(1), Some(2)], DecimalType::I32, Precision::Inexact(i256_range(-9_999, 9_999)), 4, + Precision::Inexact((-9_999, 9_999)), +)] +fn test_from_stored_decimal_records_extrema( + #[case] values: Vec>, + #[case] values_type: DecimalType, + #[case] known_range: Precision>, + #[case] child_precision: u8, + #[case] expected: Precision<(i16, i16)>, +) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let decimal_dtype = DecimalDType::new(18, 2); + let parts = stored_parts(&values, values_type, decimal_dtype); + + let result = WidenArray::from_stored_decimal(parts, known_range, &mut ctx)?; + + let child_dtype = DecimalDType::new(child_precision, 2); + let child = decimal_leaf(&result); + assert_eq!(child.decimal_dtype(), child_dtype); + let statistics = child.statistics(); + let extremum = |value: i16| { + let value = DecimalValue::try_from_i256(i256::from_i128(value.into()), child_dtype) + .vortex_expect("expected extrema fit the child precision"); + Scalar::decimal(value, child_dtype, validity_of(&values).nullability()) + }; + assert_eq!( + statistics.get(Stat::Min), + expected.map(|(min, _)| extremum(min)) + ); + assert_eq!( + statistics.get(Stat::Max), + expected.map(|(_, max)| extremum(max)) + ); + assert_arrays_eq!( + result, + canonical_reference(&values, decimal_dtype), + &mut ctx + ); + + Ok(()) +} + +#[rstest] +#[case::length_not_a_multiple_of_width( + BufferHandle::new_host(ByteBuffer::copy_from([0u8; 5])), + Validity::NonNullable, + DecimalDType::new(9, 0), + "not divisible", +)] +#[case::misaligned( + BufferHandle::new_host(ByteBuffer::copy_from([0u8; 9]).slice_unaligned(1..9)), + Validity::NonNullable, + DecimalDType::new(9, 0), + "alignment", +)] +#[case::validity_too_short( + two_i32_values(), + validity_array(&[true]), + DecimalDType::new(9, 0), + "one entry per value", +)] +#[case::validity_too_long( + two_i32_values(), + validity_array(&[true, false, true]), + DecimalDType::new(18, 0), + "one entry per value", +)] +fn test_from_stored_decimal_rejects_malformed_buffer( + #[case] values: BufferHandle, + #[case] validity: Validity, + #[case] decimal_dtype: DecimalDType, + #[case] expected: &str, +) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let parts = DecimalDataParts { + decimal_dtype, + values, + values_type: DecimalType::I32, + validity, + }; + + let error = WidenArray::from_stored_decimal(parts, Precision::Absent, &mut ctx) + .expect_err("malformed parts must fail"); + assert!(error.to_string().contains(expected), "{error}"); + + Ok(()) +} diff --git a/vortex-array/src/arrays/widen/tests/mod.rs b/vortex-array/src/arrays/widen/tests/mod.rs index 337c8bc58eb..57c8b91508d 100644 --- a/vortex-array/src/arrays/widen/tests/mod.rs +++ b/vortex-array/src/arrays/widen/tests/mod.rs @@ -13,6 +13,7 @@ mod decimal; mod decimal_aggregates; mod decimal_compare; mod decimal_fallback; +mod decimal_stored; mod encoding; mod numeric; mod serde; diff --git a/vortex-array/src/arrays/widen/vtable/plugin.rs b/vortex-array/src/arrays/widen/vtable/plugin.rs index bc2a92e42a4..5aa707c7c05 100644 --- a/vortex-array/src/arrays/widen/vtable/plugin.rs +++ b/vortex-array/src/arrays/widen/vtable/plugin.rs @@ -5,6 +5,9 @@ //! //! The node stores the child's buffer width, while its containing format supplies the logical dtype //! of the `Widen`. No `Widen` node or child decimal dtype is written. +//! +//! This uses the legacy decimal wire format because [`Widen`] has no wire representation of its +//! own. Noncanonical stored widths can therefore come from current writers as well as old files. use vortex_error::VortexResult; use vortex_error::vortex_bail; From db0c25fb96cd3b5e6433aa0a3b43d77280a88e27 Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Wed, 4 Nov 2026 14:13:49 +0000 Subject: [PATCH 13/41] Optimize arrays bottom up and as execution reaches them (#11090) With a session, the optimizer now works bottom up, and execution optimizes each node that it reaches. This prepares for lazy constructors in #11091. ## Changes - `optimize_ctx` optimizes the children of a node that were never optimized before it tries the rules of the node. It does the same for the nodes that each rewrite builds. It does not visit marked subtrees, and it marks every array that it returns. Thus inverse rules, such as `slice(get_item)` and `get_item(slice)`, cannot loop. - One optimize call rewrites a shared node only once, for example the struct below every field of a projection. - `optimize_recursive` is now the same as `optimize_ctx`. - The session-less `optimize` still rewrites only the root and does not mark the result. Thus a later `optimize_ctx` still tries the session kernels. - `execute_until` optimizes the root before it starts, each child that execution focuses, the result of each `Done` step, and the array that it returns. Single-step execution optimizes with the session, instead of trying only the static reduce and parent-reduce rules. - `Chunked::reduce` does not reduce an empty variant array, because `Variant` has no empty canonical form. --------- Signed-off-by: Robert Kruszewski --- encodings/alp/benches/alp_compress.rs | 21 +- .../datetime-parts/benches/split_temporal.rs | 3 + .../benches/dbp_assemble.rs | 6 + .../decimal-byte-parts/benches/dbp_compute.rs | 10 +- .../decimal-byte-parts/benches/dbp_split.rs | 3 + encodings/elias-fano/benches/unary.rs | 16 +- .../fastlanes/benches/bitpack_blocked.rs | 2 + .../benches/bitpack_blocked_decompress.rs | 3 + .../fastlanes/benches/bitpack_compare.rs | 4 + .../benches/bitpack_compare_sweep.rs | 4 + .../fastlanes/benches/bitpacking_filter.rs | 2 + .../fastlanes/benches/bitpacking_take.rs | 35 ++ .../fastlanes/benches/canonicalize_bench.rs | 30 +- encodings/fastlanes/benches/cast_bitpacked.rs | 8 +- .../fastlanes/benches/compute_between.rs | 20 +- encodings/fastlanes/benches/for_decode.rs | 4 + encodings/fastlanes/benches/for_encode.rs | 8 +- .../fsst/benches/chunked_dict_fsst_builder.rs | 8 +- encodings/fsst/benches/fsst_compress.rs | 20 +- encodings/fsst/benches/fsst_like.rs | 4 + encodings/fsst/benches/fsst_take.rs | 4 + encodings/fsst/benches/fsst_url_compare.rs | 15 + encodings/onpair/benches/decode.rs | 8 +- encodings/pco/benches/scalar.rs | 8 +- encodings/sparse/benches/sparse_canonical.rs | 15 +- encodings/sparse/benches/sparse_pushdown.rs | 15 +- vortex-array/benches/aggregate_grouped.rs | 8 +- vortex-array/benches/aggregate_max.rs | 66 +-- vortex-array/benches/aggregate_sum.rs | 115 ++--- vortex-array/benches/binary_ops.rs | 4 + vortex-array/benches/bool_zip.rs | 9 +- vortex-array/benches/cast_decimal.rs | 3 + vortex-array/benches/cast_primitive.rs | 27 +- vortex-array/benches/chunk_array_builder.rs | 15 + vortex-array/benches/chunked_dict_builder.rs | 5 + .../benches/chunked_fsl_canonicalize.rs | 2 + vortex-array/benches/compare.rs | 4 + vortex-array/benches/compare_sorted.rs | 4 + vortex-array/benches/dict_compare.rs | 6 + vortex-array/benches/dict_compress.rs | 7 + vortex-array/benches/dict_mask.rs | 3 + .../benches/dict_unreferenced_mask.rs | 4 + vortex-array/benches/expr/case_when_bench.rs | 17 +- vortex-array/benches/filter_bool.rs | 33 ++ vortex-array/benches/filter_fixed_width.rs | 3 + vortex-array/benches/filter_fsl.rs | 2 + vortex-array/benches/filter_list.rs | 3 + vortex-array/benches/interleave.rs | 37 +- vortex-array/benches/kleene_bool.rs | 7 + vortex-array/benches/like.rs | 21 +- vortex-array/benches/list_contains_set.rs | 9 +- vortex-array/benches/list_length.rs | 3 + vortex-array/benches/list_sum.rs | 3 + .../benches/listview_builder_extend.rs | 3 + vortex-array/benches/listview_rebuild.rs | 19 + vortex-array/benches/listview_zip.rs | 9 +- vortex-array/benches/patches_lookup.rs | 6 + .../piecewise_sequence_take_primitive.rs | 5 + vortex-array/benches/primitive_zip.rs | 9 +- vortex-array/benches/probe.rs | 3 + vortex-array/benches/row_fn_bool_retry.rs | 4 + vortex-array/benches/row_fn_output.rs | 5 + vortex-array/benches/row_fn_utf8_input.rs | 5 +- vortex-array/benches/run_end_compress.rs | 6 + vortex-array/benches/run_end_decode.rs | 8 + .../benches/run_end_decode_primitive.rs | 5 + vortex-array/benches/run_end_null_count.rs | 9 +- vortex-array/benches/run_end_sum.rs | 3 + vortex-array/benches/run_end_take.rs | 3 + vortex-array/benches/scalar_at_struct.rs | 3 + vortex-array/benches/scalar_fn_probe.rs | 15 +- vortex-array/benches/scalar_subtract.rs | 2 + vortex-array/benches/take_chunked.rs | 3 + vortex-array/benches/take_filter.rs | 25 + vortex-array/benches/take_fsl.rs | 3 + vortex-array/benches/take_patches.rs | 28 +- vortex-array/benches/take_primitive.rs | 6 + vortex-array/benches/take_struct.rs | 7 + vortex-array/benches/take_varbin.rs | 3 + vortex-array/benches/validity_is_valid.rs | 10 +- vortex-array/benches/varbinview_compact.rs | 3 + vortex-array/benches/varbinview_fill_null.rs | 2 + vortex-array/benches/varbinview_zip.rs | 15 +- vortex-array/benches/widen.rs | 36 +- vortex-array/benches/widen_decimal.rs | 9 +- vortex-array/src/array/erased.rs | 5 + vortex-array/src/arrays/chunked/vtable/mod.rs | 2 + vortex-array/src/executor.rs | 66 ++- vortex-array/src/optimizer/mod.rs | 431 +++++++++++++----- vortex-array/src/test_harness/mod.rs | 37 ++ vortex-array/src/test_harness/trace/mod.rs | 34 +- vortex-array/src/test_harness/trace/tests.rs | 42 +- vortex-array/src/trace_macros.rs | 11 +- vortex-arrow/benches/to_arrow.rs | 17 +- vortex-btrblocks/benches/compress.rs | 25 +- vortex-btrblocks/benches/compress_listview.rs | 3 + vortex-compressor/benches/dict_encode.rs | 3 + vortex-compressor/benches/float_stats.rs | 11 +- vortex-compressor/benches/integer_stats.rs | 11 +- vortex-compressor/benches/stats_calc.rs | 10 +- vortex-cuda/benches/alp_cuda.rs | 2 + vortex-cuda/benches/arrow_binary_cuda.rs | 3 + vortex-cuda/benches/arrow_validity_cuda.rs | 3 + vortex-cuda/benches/bitpacked_cuda.rs | 4 + vortex-cuda/benches/date_time_parts_cuda.rs | 2 + vortex-cuda/benches/delta_cuda.rs | 2 + vortex-cuda/benches/dict_cuda.rs | 2 + vortex-cuda/benches/for_cuda.rs | 3 + vortex-cuda/benches/fsst_cuda.rs | 7 +- vortex-cuda/benches/list_view_cuda.rs | 5 + vortex-cuda/benches/onpair_cuda.rs | 2 + vortex-cuda/benches/runend_cuda.rs | 2 + vortex-duckdb/benches/decimal_export.rs | 2 + vortex-layout/benches/zone_map_prune.rs | 2 + vortex-row/benches/row_encode.rs | 13 +- vortex-spatial/benches/area.rs | 3 + vortex-spatial/benches/binary_predicates.rs | 47 +- vortex-spatial/benches/collect.rs | 5 + vortex-spatial/benches/convex_hull.rs | 3 + vortex-spatial/benches/distance.rs | 4 + vortex-spatial/benches/envelope.rs | 15 + vortex-spatial/benches/length.rs | 7 + vortex-spatial/benches/make_line.rs | 10 + vortex-tensor/benches/cosine_similarity.rs | 23 +- vortex-tensor/benches/inner_product.rs | 13 +- vortex-tensor/benches/l2_norm.rs | 9 +- .../common_encoding_tree_throughput.rs | 3 + vortex/benches/single_encoding_throughput.rs | 36 +- vortex/benches/take_union.rs | 4 + 129 files changed, 1430 insertions(+), 482 deletions(-) diff --git a/encodings/alp/benches/alp_compress.rs b/encodings/alp/benches/alp_compress.rs index 263f0ed45c9..7f551a93bc4 100644 --- a/encodings/alp/benches/alp_compress.rs +++ b/encodings/alp/benches/alp_compress.rs @@ -25,6 +25,7 @@ use vortex_array::dtype::NativePType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_buffer::buffer; @@ -100,6 +101,7 @@ fn compress_alp(bencher: Bencher, args: (usize, f64, }; let values = values.freeze(); let array = PrimitiveArray::new(values, validity); + mark_optimized(array.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&array, SESSION.create_execution_ctx())) @@ -126,15 +128,15 @@ fn decompress_alp(bencher: Bencher, args: (usize, f64 let values = values.freeze(); bencher .with_inputs(|| { - ( - alp_encode( - PrimitiveArray::new(Buffer::copy_from(&values), validity.clone()).as_view(), - None, - &mut SESSION.create_execution_ctx(), - ) - .unwrap(), - SESSION.create_execution_ctx(), + let encoded = alp_encode( + PrimitiveArray::new(Buffer::copy_from(&values), validity.clone()).as_view(), + None, + &mut SESSION.create_execution_ctx(), ) + .unwrap(); + mark_optimized(encoded.as_array(), &SESSION).unwrap(); + + (encoded, SESSION.create_execution_ctx()) }) .bench_values(|(v, mut ctx)| decompress_into_array(v, &mut ctx)); } @@ -179,6 +181,8 @@ fn make_rd_array(n: usize, fraction_patch: f64) -> fn compress_rd(bencher: Bencher, args: (usize, f64)) { let (n, fraction_patch) = args; let primitive = make_rd_array::(n, fraction_patch); + mark_optimized(primitive.as_array(), &SESSION).unwrap(); + let encoder = RDEncoder::new(primitive.as_slice::()); bencher @@ -192,6 +196,7 @@ fn decompress_rd(bencher: Bencher, args: (usize, f6 let primitive = make_rd_array::(n, fraction_patch); let encoder = RDEncoder::new(primitive.as_slice::()); let encoded = encoder.encode(primitive.as_view()); + mark_optimized(encoded.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&encoded, SESSION.create_execution_ctx())) diff --git a/encodings/datetime-parts/benches/split_temporal.rs b/encodings/datetime-parts/benches/split_temporal.rs index e7b473cbef3..9349969a5de 100644 --- a/encodings/datetime-parts/benches/split_temporal.rs +++ b/encodings/datetime-parts/benches/split_temporal.rs @@ -16,6 +16,7 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::TemporalArray; use vortex_array::extension::datetime::TimeUnit; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_datetime_parts::split_temporal; @@ -58,6 +59,7 @@ fn split(bencher: Bencher, args: (usize, TimeUnit)) { unit, Some("UTC".into()), ); + mark_optimized(array.temporal_values(), &SESSION).unwrap(); bencher .with_inputs(|| (array.clone(), SESSION.create_execution_ctx())) @@ -80,6 +82,7 @@ fn split_extreme(bencher: Bencher, args: (usize, TimeUnit)) { unit, Some("UTC".into()), ); + mark_optimized(array.temporal_values(), &SESSION).unwrap(); bencher .with_inputs(|| (array.clone(), SESSION.create_execution_ctx())) diff --git a/encodings/decimal-byte-parts/benches/dbp_assemble.rs b/encodings/decimal-byte-parts/benches/dbp_assemble.rs index 33ab5efbf2a..582d4bb52dc 100644 --- a/encodings/decimal-byte-parts/benches/dbp_assemble.rs +++ b/encodings/decimal-byte-parts/benches/dbp_assemble.rs @@ -114,6 +114,7 @@ mod arrays { use vortex_array::dtype::DecimalDType; use vortex_array::dtype::DecimalType; use vortex_array::optimizer::kernels::KernelSession; + use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::buffer; use vortex_decimal_byte_parts::_benchmarking::assemble_decimal; @@ -170,6 +171,11 @@ mod arrays { lower_parts: Vec, decimal_dtype: DecimalDType, ) { + mark_optimized(&msp, &SESSION).vortex_expect("mark the MSP as optimized"); + for part in &lower_parts { + mark_optimized(part, &SESSION).vortex_expect("mark the lower part as optimized"); + } + bencher .with_inputs(|| SESSION.create_execution_ctx()) .bench_refs(|ctx| { diff --git a/encodings/decimal-byte-parts/benches/dbp_compute.rs b/encodings/decimal-byte-parts/benches/dbp_compute.rs index f206f2eb505..2bbe20fed68 100644 --- a/encodings/decimal-byte-parts/benches/dbp_compute.rs +++ b/encodings/decimal-byte-parts/benches/dbp_compute.rs @@ -39,6 +39,7 @@ use vortex_array::dtype::PType; use vortex_array::scalar::DecimalValue; use vortex_array::scalar::Scalar; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_buffer::Buffer; @@ -219,9 +220,12 @@ impl Input { .map_or(Validity::NonNullable, Validity::from); let msp = PrimitiveArray::from_byte_buffer(self.msp_values.clone(), self.msp_ptype, validity); - DecimalByteParts::try_new(msp.into_array(), self.decimal_dtype) + let array = DecimalByteParts::try_new(msp.into_array(), self.decimal_dtype) .vortex_expect("the MSP of every benchmark input is a signed integer array") - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).vortex_expect("mark the benchmark input as optimized"); + + array } } @@ -257,6 +261,8 @@ fn decode(bencher: Bencher, case: Case) { /// Compares a new array of `case` with `Gte` against a constant of `value` in each iteration. fn bench_compare(bencher: Bencher, case: Case, value: DecimalValue) { let constant = case.constant(value); + mark_optimized(&constant, &SESSION).vortex_expect("mark the constant as optimized"); + bench_case(bencher, case, |array, ctx| { array .binary(constant.clone(), Operator::Gte) diff --git a/encodings/decimal-byte-parts/benches/dbp_split.rs b/encodings/decimal-byte-parts/benches/dbp_split.rs index 933eb31a519..d38371bbd8a 100644 --- a/encodings/decimal-byte-parts/benches/dbp_split.rs +++ b/encodings/decimal-byte-parts/benches/dbp_split.rs @@ -86,6 +86,7 @@ mod arrays { use vortex_array::VortexSessionExecute; use vortex_array::dtype::DecimalType; use vortex_array::optimizer::kernels::KernelSession; + use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_decimal_byte_parts::split_decimal; use vortex_error::VortexExpect; @@ -116,6 +117,8 @@ mod arrays { fn bench_split(bencher: Bencher, values_type: DecimalType, len: usize, validity: Validity) { let decimal = decimal_array(values_type, len, validity); + mark_optimized(decimal.as_array(), &SESSION).vortex_expect("mark the decimal as optimized"); + bencher .with_inputs(|| SESSION.create_execution_ctx()) .bench_refs(|ctx| { diff --git a/encodings/elias-fano/benches/unary.rs b/encodings/elias-fano/benches/unary.rs index b52177c62da..1a93e6a77bb 100644 --- a/encodings/elias-fano/benches/unary.rs +++ b/encodings/elias-fano/benches/unary.rs @@ -34,6 +34,7 @@ use vortex_array::IntoArray; use vortex_array::VortexSessionExecute; use vortex_array::arrays::Primitive; use vortex_array::arrays::PrimitiveArray; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_elias_fano::unary_encode; @@ -106,13 +107,16 @@ fn primitive(shape: Shape) -> PrimitiveArray { } fn encoded(shape: Shape) -> ArrayRef { - unary_encode( + let array = unary_encode( primitive(shape).as_ref().as_::(), &mut SESSION.create_execution_ctx(), ) .unwrap() .unwrap() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } #[divan::bench(args = SHAPES)] @@ -121,10 +125,10 @@ fn encode(bencher: Bencher, shape: Shape) { // A fresh array each time, so `sum` cannot reuse the statistic cached on the last one. bencher .with_inputs(|| { - ( - PrimitiveArray::new(values.clone(), Validity::NonNullable), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::new(values.clone(), Validity::NonNullable); + mark_optimized(array.as_array(), &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_local_values(|(array, mut ctx)| { divan::black_box(unary_encode(array.as_ref().as_::(), &mut ctx).unwrap()); diff --git a/encodings/fastlanes/benches/bitpack_blocked.rs b/encodings/fastlanes/benches/bitpack_blocked.rs index 2f9e217ce95..297c82a80d8 100644 --- a/encodings/fastlanes/benches/bitpack_blocked.rs +++ b/encodings/fastlanes/benches/bitpack_blocked.rs @@ -15,6 +15,7 @@ use vortex_array::aggregate_fn::session::AggregateFnSession; use vortex_array::arrays::PrimitiveArray; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_fastlanes::bitpack_compress::bitpack_encode_blocked; use vortex_session::VortexSession; @@ -54,6 +55,7 @@ fn bitpack_blocked_compress(bencher: Bencher) { let array = PrimitiveArray::from_iter(bit_widths.iter().flat_map(|&bit_width| { (0..1024u32).map(move |i| i.wrapping_mul(7919) & ((1 << bit_width) - 1)) })); + mark_optimized(array.as_array(), &SESSION).unwrap(); bencher.counter(ItemsCount::new(array.len())).bench(|| { bitpack_encode_blocked( diff --git a/encodings/fastlanes/benches/bitpack_blocked_decompress.rs b/encodings/fastlanes/benches/bitpack_blocked_decompress.rs index 49cd88ebe80..133daffb397 100644 --- a/encodings/fastlanes/benches/bitpack_blocked_decompress.rs +++ b/encodings/fastlanes/benches/bitpack_blocked_decompress.rs @@ -18,6 +18,7 @@ use vortex_array::dtype::NativePType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_fastlanes::BitPackedArraySlotsExt; use vortex_fastlanes::bitpack_compress::bitpack_encode_blocked; use vortex_fastlanes::bitpack_decompress::unpack_array_blocked; @@ -64,6 +65,8 @@ where &mut SESSION.create_execution_ctx(), ) .unwrap(); + mark_optimized(array.as_array(), &SESSION).unwrap(); + let offsets = array.block_offsets().unwrap().clone(); bencher.counter(ItemsCount::new(array.len())).bench(|| { diff --git a/encodings/fastlanes/benches/bitpack_compare.rs b/encodings/fastlanes/benches/bitpack_compare.rs index 9cfc5cb5201..935e89b8b12 100644 --- a/encodings/fastlanes/benches/bitpack_compare.rs +++ b/encodings/fastlanes/benches/bitpack_compare.rs @@ -26,6 +26,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::fns::operators::Operator; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Alignment; use vortex_buffer::BufferMut; @@ -98,6 +99,9 @@ fn build_inputs() -> (ArrayRef, ArrayRef, ExecutionCtx) { // 1 << BW is just past the packable range, so the out-of-range fast path fires. let constant = 1u32 << BW; let rhs = ConstantArray::new(constant, LEN).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + mark_optimized(&rhs, &SESSION).unwrap(); + (array, rhs, ctx) } diff --git a/encodings/fastlanes/benches/bitpack_compare_sweep.rs b/encodings/fastlanes/benches/bitpack_compare_sweep.rs index c5c471f6ac1..7c478245b93 100644 --- a/encodings/fastlanes/benches/bitpack_compare_sweep.rs +++ b/encodings/fastlanes/benches/bitpack_compare_sweep.rs @@ -31,6 +31,7 @@ use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; use vortex_array::scalar_fn::fns::operators::Operator; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Alignment; use vortex_buffer::BufferMut; @@ -141,6 +142,9 @@ fn setup(width: usize) -> (ArrayRef, ArrayRef, ExecutionCtx) { ) .into_array(); let rhs = ConstantArray::new(T::from_counter(cap / 2), LEN).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + mark_optimized(&rhs, &SESSION).unwrap(); + (array, rhs, ctx) } diff --git a/encodings/fastlanes/benches/bitpacking_filter.rs b/encodings/fastlanes/benches/bitpacking_filter.rs index d9d6ecac46f..e6eb9125a6a 100644 --- a/encodings/fastlanes/benches/bitpacking_filter.rs +++ b/encodings/fastlanes/benches/bitpacking_filter.rs @@ -18,6 +18,7 @@ use vortex_array::aggregate_fn::session::AggregateFnSession; use vortex_array::arrays::PrimitiveArray; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BufferMut; use vortex_fastlanes::BitPackedData; @@ -61,6 +62,7 @@ fn fixture(selected_per_chunk: usize) -> (ArrayRef, Mask) { ) .unwrap() .into_array(); + mark_optimized(&packed, &SESSION).unwrap(); let indices = (0..NUM_SELECTED_CHUNKS).flat_map(|chunk| { (0..selected_per_chunk) diff --git a/encodings/fastlanes/benches/bitpacking_take.rs b/encodings/fastlanes/benches/bitpacking_take.rs index 6146f82fda2..0948ab6f672 100644 --- a/encodings/fastlanes/benches/bitpacking_take.rs +++ b/encodings/fastlanes/benches/bitpacking_take.rs @@ -27,6 +27,7 @@ use vortex_array::dtype::PType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_buffer::BufferMut; @@ -80,6 +81,9 @@ fn threshold_fixture(selected_per_chunk: usize) -> (ArrayRef, ArrayRef) { .map(move |index| (chunk * CHUNK_SIZE + index * CHUNK_SIZE / selected_per_chunk) as u32) })) .into_array(); + mark_optimized(&packed, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); + (packed, indices) } @@ -115,6 +119,8 @@ fn take_10_stratified(bencher: Bencher) { let packed = bitpack_to_best_bit_width(&uncompressed, &mut SESSION.create_execution_ctx()).unwrap(); let indices = PrimitiveArray::from_iter((0..10).map(|i| i * 6_553)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -134,6 +140,8 @@ fn take_10_contiguous(bencher: Bencher) { let packed = bitpack_to_best_bit_width(&uncompressed, &mut SESSION.create_execution_ctx()).unwrap(); let indices = buffer![0..10].into_array(); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -156,6 +164,8 @@ fn take_10k_random(bencher: Bencher) { let rng = StdRng::seed_from_u64(0); let indices = PrimitiveArray::from_iter(rng.sample_iter(range).take(10_000).map(|i| i as u32)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -175,6 +185,8 @@ fn take_10k_contiguous(bencher: Bencher) { let packed = bitpack_to_best_bit_width(&uncompressed, &mut SESSION.create_execution_ctx()).unwrap(); let indices = PrimitiveArray::from_iter(0..10_000); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -194,6 +206,8 @@ fn take_10k_dispersed(bencher: Bencher) { let packed = bitpack_to_best_bit_width(&uncompressed, &mut SESSION.create_execution_ctx()).unwrap(); let indices = PrimitiveArray::from_iter((0..10_000).map(|i| (i * 42) % values.len() as u64)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -213,6 +227,8 @@ fn take_10k_first_chunk_only(bencher: Bencher) { let packed = bitpack_to_best_bit_width(&uncompressed, &mut SESSION.create_execution_ctx()).unwrap(); let indices = PrimitiveArray::from_iter((0..10_000).map(|i| ((i * 42) % 1024) as u64)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -259,6 +275,8 @@ fn patched_take_10_stratified(bencher: Bencher) { ); let indices = PrimitiveArray::from_iter((0..10).map(|i| i * 6_653)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -285,6 +303,8 @@ fn patched_take_10_contiguous(bencher: Bencher) { ); let indices = buffer![0..10].into_array(); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -307,6 +327,8 @@ fn patched_take_10k_random(bencher: Bencher) { let rng = StdRng::seed_from_u64(0); let range = Uniform::new(0, values.len()).unwrap(); let indices = PrimitiveArray::from_iter(rng.sample_iter(range).take(10_000).map(|i| i as u32)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -326,6 +348,8 @@ fn patched_take_10k_contiguous_not_patches(bencher: Bencher) { let packed = bitpack_to_best_bit_width(&uncompressed, &mut SESSION.create_execution_ctx()).unwrap(); let indices = PrimitiveArray::from_iter((0u32..NUM_EXCEPTIONS).cycle().take(10000)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -353,6 +377,8 @@ fn patched_take_10k_contiguous_patches(bencher: Bencher) { let indices = PrimitiveArray::from_iter((BIG_BASE2..BIG_BASE2 + NUM_EXCEPTIONS).cycle().take(10000)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -372,6 +398,8 @@ fn patched_take_10k_dispersed(bencher: Bencher) { let packed = bitpack_to_best_bit_width(&uncompressed, &mut SESSION.create_execution_ctx()).unwrap(); let indices = PrimitiveArray::from_iter((0..10_000).map(|i| (i * 42) % values.len() as u64)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -391,6 +419,8 @@ fn patched_take_10k_first_chunk_only(bencher: Bencher) { let packed = bitpack_to_best_bit_width(&uncompressed, &mut SESSION.create_execution_ctx()).unwrap(); let indices = PrimitiveArray::from_iter((0..10_000).map(|i| ((i * 42) % 1024) as u64)); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -417,6 +447,8 @@ fn patched_take_10k_adversarial(bencher: Bencher) { .flat_map(|base_idx| base_idx..(base_idx + per_chunk_count)) .take(10000), ); + mark_optimized(packed.as_array(), &SESSION).unwrap(); + mark_optimized(indices.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&packed, &indices, SESSION.create_execution_ctx())) @@ -550,6 +582,9 @@ fn isolated_runs(bencher: Bencher, (run_length, offset): (usize, } fn bench_piecewise_take(bencher: Bencher, packed: ArrayRef, indices: ArrayRef) { + mark_optimized(&packed, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); + bencher .counter(ItemsCount::new(indices.len())) .with_inputs(|| (indices.clone(), SESSION.create_execution_ctx())) diff --git a/encodings/fastlanes/benches/canonicalize_bench.rs b/encodings/fastlanes/benches/canonicalize_bench.rs index b96fa530e8f..a4ff901c23f 100644 --- a/encodings/fastlanes/benches/canonicalize_bench.rs +++ b/encodings/fastlanes/benches/canonicalize_bench.rs @@ -17,6 +17,8 @@ use vortex_array::builders::PrimitiveBuilder; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_decoded; +use vortex_array::test_harness::mark_optimized; use vortex_error::VortexExpect; use vortex_fastlanes::bitpack_compress::test_harness::make_array; use vortex_session::VortexSession; @@ -82,13 +84,16 @@ fn into_canonical_non_nullable( .vortex_expect("make_array works") }) .collect::>(); + for chunk in &chunks { + mark_optimized(chunk, &SESSION).vortex_expect("mark the chunk as optimized"); + } bencher .with_inputs(|| { - ( - ChunkedArray::from_iter(chunks.clone()).into_array(), - SESSION.create_execution_ctx(), - ) + let chunked = ChunkedArray::from_iter(chunks.clone()).into_array(); + mark_decoded(&chunked); + + (chunked, SESSION.create_execution_ctx()) }) .bench_refs(|(chunked, ctx)| chunked.clone().execute::(ctx)); } @@ -112,6 +117,9 @@ fn canonical_into_non_nullable( .vortex_expect("make_array works") }) .collect::>(); + for chunk in &chunks { + mark_optimized(chunk, &SESSION).vortex_expect("mark the chunk as optimized"); + } bencher .with_inputs(|| { @@ -170,13 +178,16 @@ fn into_canonical_nullable( .vortex_expect("make_array works") }) .collect::>(); + for chunk in &chunks { + mark_optimized(chunk, &SESSION).vortex_expect("mark the chunk as optimized"); + } bencher .with_inputs(|| { - ( - ChunkedArray::from_iter(chunks.clone()).into_array(), - SESSION.create_execution_ctx(), - ) + let chunked = ChunkedArray::from_iter(chunks.clone()).into_array(); + mark_decoded(&chunked); + + (chunked, SESSION.create_execution_ctx()) }) .bench_values(|(chunked, mut ctx)| chunked.execute::(&mut ctx)); } @@ -200,6 +211,9 @@ fn canonical_into_nullable( .vortex_expect("make_array works") }) .collect::>(); + for chunk in &chunks { + mark_optimized(chunk, &SESSION).vortex_expect("mark the chunk as optimized"); + } bencher .with_inputs(|| { diff --git a/encodings/fastlanes/benches/cast_bitpacked.rs b/encodings/fastlanes/benches/cast_bitpacked.rs index efc82e464db..e5e3495bf11 100644 --- a/encodings/fastlanes/benches/cast_bitpacked.rs +++ b/encodings/fastlanes/benches/cast_bitpacked.rs @@ -26,6 +26,7 @@ use vortex_array::dtype::PType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BufferMut; use vortex_error::VortexExpect; @@ -94,11 +95,14 @@ fn make_chunks(len: usize, count: usize, fraction_patched: f64) -> Vec ArrayRef { - if chunks.len() == 1 { + let array = if chunks.len() == 1 { chunks[0].clone().into_array() } else { ChunkedArray::from_iter(chunks.iter().map(|c| c.clone().into_array())).into_array() - } + }; + mark_optimized(&array, &SESSION).vortex_expect("mark the array as optimized"); + + array } #[divan::bench(args = ARGS)] diff --git a/encodings/fastlanes/benches/compute_between.rs b/encodings/fastlanes/benches/compute_between.rs index 3c593dc006e..8b9ed8b6255 100644 --- a/encodings/fastlanes/benches/compute_between.rs +++ b/encodings/fastlanes/benches/compute_between.rs @@ -21,6 +21,7 @@ use vortex_array::dtype::NativePType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_error::VortexExpect; use vortex_fastlanes::bitpack_compress::bitpack_to_best_bit_width; use vortex_session::VortexSession; @@ -47,9 +48,12 @@ fn generate_primitive_array( rng: &mut StdRng, len: usize, ) -> PrimitiveArray { - (0..len) + let array = (0..len) .map(|_| T::from_usize(rng.random_range(0..10_000)).vortex_expect("")) - .collect::() + .collect::(); + mark_optimized(array.as_array(), &SESSION).unwrap(); + + array } fn generate_bit_pack_primitive_array( @@ -60,9 +64,12 @@ fn generate_bit_pack_primitive_array( .map(|_| T::from_usize(rng.random_range(0..10_000)).vortex_expect("")) .collect::(); - bitpack_to_best_bit_width(&a, &mut SESSION.create_execution_ctx()) + let array = bitpack_to_best_bit_width(&a, &mut SESSION.create_execution_ctx()) .vortex_expect("") - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } fn generate_alp_bit_pack_primitive_array( @@ -85,7 +92,10 @@ fn generate_alp_bit_pack_primitive_array( let bp = bitpack_to_best_bit_width(&encoded, &mut ctx) .vortex_expect("") .into_array(); - ALP::new(bp, alp.exponents(), None).into_array() + let array = ALP::new(bp, alp.exponents(), None).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } const BENCH_ARGS: &[usize] = if cfg!(codspeed) { diff --git a/encodings/fastlanes/benches/for_decode.rs b/encodings/fastlanes/benches/for_decode.rs index 83d877f9bd3..d2472d2951e 100644 --- a/encodings/fastlanes/benches/for_decode.rs +++ b/encodings/fastlanes/benches/for_decode.rs @@ -29,6 +29,7 @@ use vortex_array::dtype::NativePType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_error::vortex_panic; @@ -108,6 +109,8 @@ fn bitpacked_for_array>(len: usize, child: Child fn run>(bencher: Bencher, bytes: usize, child: Child) { let len = bytes / size_of::(); let array = bitpacked_for_array::(len, child); + mark_optimized(&array, &SESSION).unwrap(); + bencher .counter(ItemsCount::new(len)) .with_inputs(|| (&array, SESSION.create_execution_ctx())) @@ -140,6 +143,7 @@ fn decode_chunked>(bencher: Bencher, bytes: usiz let array = FoR::try_new_chunked(encoded.into_array(), references.clone(), 0) .unwrap() .into_array(); + mark_optimized(&array, &SESSION).unwrap(); (array, SESSION.create_execution_ctx()) }) diff --git a/encodings/fastlanes/benches/for_encode.rs b/encodings/fastlanes/benches/for_encode.rs index 16676ca16cb..e78e8e41a03 100644 --- a/encodings/fastlanes/benches/for_encode.rs +++ b/encodings/fastlanes/benches/for_encode.rs @@ -23,6 +23,7 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::dtype::NativePType; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_fastlanes::FoR; @@ -74,7 +75,12 @@ fn run>(bencher: Bencher, bytes: usize, nullable bencher .counter(ItemsCount::new(len)) // `encode_chunked` takes the array by value, so each iteration encodes a fresh one. - .with_inputs(|| PrimitiveArray::new(buffer.clone(), validity.clone())) + .with_inputs(|| { + let array = PrimitiveArray::new(buffer.clone(), validity.clone()); + mark_optimized(array.as_array(), &SESSION).unwrap(); + + array + }) .bench_local_values(|array| { FoR::encode_chunked(array, &mut SESSION.create_execution_ctx()).unwrap() }); diff --git a/encodings/fsst/benches/chunked_dict_fsst_builder.rs b/encodings/fsst/benches/chunked_dict_fsst_builder.rs index f599fdcc2d5..72637ce578c 100644 --- a/encodings/fsst/benches/chunked_dict_fsst_builder.rs +++ b/encodings/fsst/benches/chunked_dict_fsst_builder.rs @@ -15,6 +15,7 @@ use vortex_array::dtype::NativePType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_error::VortexExpect; use vortex_fsst::test_utils::gen_dict_fsst_test_data; use vortex_session::VortexSession; @@ -52,10 +53,13 @@ fn make_dict_fsst_chunks( chunk_count: usize, ) -> ArrayRef { let mut ctx = SESSION.create_execution_ctx(); - (0..chunk_count) + let array = (0..chunk_count) .map(|_| gen_dict_fsst_test_data::(len, unique_values, 20, 30, &mut ctx).into_array()) .collect::() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).vortex_expect("mark the chunked array as optimized"); + + array } #[divan::bench(args = BENCH_ARGS)] diff --git a/encodings/fsst/benches/fsst_compress.rs b/encodings/fsst/benches/fsst_compress.rs index a3b9ee0b9d9..f24db0ba547 100644 --- a/encodings/fsst/benches/fsst_compress.rs +++ b/encodings/fsst/benches/fsst_compress.rs @@ -29,6 +29,7 @@ use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; use vortex_array::scalar_fn::fns::operators::Operator; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_fsst::fsst_compress; use vortex_fsst::fsst_train_compressor; use vortex_session::VortexSession; @@ -87,6 +88,7 @@ fn decompress_fsst(bencher: Bencher, (string_count, avg_len, unique_chars): (usi let encoded = fsst_compress(&array, &compressor, &mut ctx) .unwrap() .into_array(); + mark_optimized(&encoded, &SESSION).unwrap(); bencher .with_inputs(|| (&encoded, SESSION.create_execution_ctx())) @@ -103,6 +105,8 @@ fn pushdown_compare(bencher: Bencher, (string_count, avg_len, unique_chars): (us .unwrap() .into_array(); let constant = ConstantArray::new(needle(&array, &mut ctx), len); + mark_optimized(&fsst_array, &SESSION).unwrap(); + mark_optimized(constant.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, &constant, SESSION.create_execution_ctx())) @@ -129,6 +133,8 @@ fn canonicalize_compare( .unwrap() .into_array(); let constant = ConstantArray::new(needle(&array, &mut ctx), len); + mark_optimized(&fsst_array, &SESSION).unwrap(); + mark_optimized(constant.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, &constant, SESSION.create_execution_ctx())) @@ -215,13 +221,16 @@ fn generate_test_data(string_count: usize, avg_len: usize, unique_chars: u8) -> )); } - VarBinArray::from_iter( + let array = VarBinArray::from_iter( strings .into_iter() .map(|opt_s| opt_s.map(Vec::into_boxed_slice)), DType::Binary(Nullability::NonNullable), ) - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } fn generate_chunked_test_data( @@ -231,7 +240,7 @@ fn generate_chunked_test_data( unique_chars: u8, ) -> ArrayRef { let mut ctx = SESSION.create_execution_ctx(); - (0..chunk_size) + let array = (0..chunk_size) .map(|_| { let array = generate_test_data(string_count, avg_len, unique_chars); let compressor = fsst_train_compressor(&array, &mut ctx).unwrap(); @@ -240,5 +249,8 @@ fn generate_chunked_test_data( .into_array() }) .collect::() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } diff --git a/encodings/fsst/benches/fsst_like.rs b/encodings/fsst/benches/fsst_like.rs index a6336292ef2..7f875c7fe07 100644 --- a/encodings/fsst/benches/fsst_like.rs +++ b/encodings/fsst/benches/fsst_like.rs @@ -17,6 +17,7 @@ use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::fns::like::Like; use vortex_array::scalar_fn::fns::like::LikeOptions; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_fsst::FSSTArray; use vortex_fsst::test_utils::NUM_STRINGS; use vortex_fsst::test_utils::make_fsst_clickbench_urls; @@ -128,6 +129,9 @@ fn bench_like(bencher: Bencher, fsst: &FSSTArray, pattern: &str) { let len = fsst.len(); let arr = fsst.clone().into_array(); let pattern = ConstantArray::new(pattern, len).into_array(); + mark_optimized(&arr, &SESSION).unwrap(); + mark_optimized(&pattern, &SESSION).unwrap(); + bencher .with_inputs(|| SESSION.create_execution_ctx()) .bench_refs(|ctx| { diff --git a/encodings/fsst/benches/fsst_take.rs b/encodings/fsst/benches/fsst_take.rs index 80f483b6289..3d6896ce5f4 100644 --- a/encodings/fsst/benches/fsst_take.rs +++ b/encodings/fsst/benches/fsst_take.rs @@ -23,6 +23,7 @@ use vortex_array::dtype::Nullability; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_fsst::FSSTArray; use vortex_fsst::fsst_compress; use vortex_fsst::fsst_train_compressor; @@ -120,6 +121,9 @@ fn take(bencher: Bencher, args: (usize, usize, usize)) { } fn bench_take(bencher: Bencher, array: ArrayRef, indices: ArrayRef) { + mark_optimized(&array, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); + bencher .with_inputs(|| (&array, &indices, SESSION.create_execution_ctx())) .bench_refs(|(array, indices, ctx)| { diff --git a/encodings/fsst/benches/fsst_url_compare.rs b/encodings/fsst/benches/fsst_url_compare.rs index f62af505f3d..4719387cc0d 100644 --- a/encodings/fsst/benches/fsst_url_compare.rs +++ b/encodings/fsst/benches/fsst_url_compare.rs @@ -24,6 +24,7 @@ use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; use vortex_array::scalar_fn::fns::operators::Operator; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_fsst::fsst_compress; use vortex_fsst::fsst_train_compressor; use vortex_fsst::test_utils::HIGH_MATCH_DOMAIN; @@ -93,6 +94,8 @@ fn eq_pushdown_high_match(bencher: Bencher) { .into_array(); let match_url = pick_url_with_domain(data, HIGH_MATCH_DOMAIN); let constant = ConstantArray::new(Scalar::from(match_url.as_str()), NUM_URLS); + mark_optimized(&fsst_array, &SESSION).unwrap(); + mark_optimized(constant.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, &constant, SESSION.create_execution_ctx())) @@ -117,6 +120,8 @@ fn eq_pushdown_low_match(bencher: Bencher) { .into_array(); let match_url = pick_url_with_domain(data, LOW_MATCH_DOMAIN); let constant = ConstantArray::new(Scalar::from(match_url.as_str()), NUM_URLS); + mark_optimized(&fsst_array, &SESSION).unwrap(); + mark_optimized(constant.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, &constant, SESSION.create_execution_ctx())) @@ -141,6 +146,8 @@ fn eq_pushdown_high_match_view(bencher: Bencher) { .into_array(); let match_url = pick_url_with_domain(&URL_DATA, HIGH_MATCH_DOMAIN); let constant = ConstantArray::new(Scalar::from(match_url.as_str()), NUM_URLS); + mark_optimized(&fsst_array, &SESSION).unwrap(); + mark_optimized(constant.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, &constant, SESSION.create_execution_ctx())) @@ -163,6 +170,8 @@ fn eq_pushdown_low_match_view(bencher: Bencher) { let fsst_array = fsst_compress(&array, &compressor, &mut ctx).unwrap(); let match_url = pick_url_with_domain(&URL_DATA, LOW_MATCH_DOMAIN); let constant = ConstantArray::new(Scalar::from(match_url.as_str()), NUM_URLS); + mark_optimized(fsst_array.as_array(), &SESSION).unwrap(); + mark_optimized(constant.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, &constant, SESSION.create_execution_ctx())) @@ -188,6 +197,8 @@ fn eq_canonicalize_high_match(bencher: Bencher) { .into_array(); let match_url = pick_url_with_domain(data, HIGH_MATCH_DOMAIN); let constant = ConstantArray::new(Scalar::from(match_url.as_str()), NUM_URLS); + mark_optimized(&fsst_array, &SESSION).unwrap(); + mark_optimized(constant.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, &constant, SESSION.create_execution_ctx())) @@ -215,6 +226,8 @@ fn eq_canonicalize_low_match(bencher: Bencher) { .into_array(); let match_url = pick_url_with_domain(data, LOW_MATCH_DOMAIN); let constant = ConstantArray::new(Scalar::from(match_url.as_str()), NUM_URLS); + mark_optimized(&fsst_array, &SESSION).unwrap(); + mark_optimized(constant.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, &constant, SESSION.create_execution_ctx())) @@ -246,6 +259,7 @@ fn like_substr_high_match(bencher: Bencher) { .into_array(); let pattern = format!("%{HIGH_MATCH_DOMAIN}%"); let expr = like(root(), lit(pattern.as_str())); + mark_optimized(&fsst_array, &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, SESSION.create_execution_ctx())) @@ -270,6 +284,7 @@ fn like_substr_low_match(bencher: Bencher) { .into_array(); let pattern = format!("%{LOW_MATCH_DOMAIN}%"); let expr = like(root(), lit(pattern.as_str())); + mark_optimized(&fsst_array, &SESSION).unwrap(); bencher .with_inputs(|| (&fsst_array, SESSION.create_execution_ctx())) diff --git a/encodings/onpair/benches/decode.rs b/encodings/onpair/benches/decode.rs index 931db12bc06..c69f4ea163a 100644 --- a/encodings/onpair/benches/decode.rs +++ b/encodings/onpair/benches/decode.rs @@ -82,6 +82,7 @@ impl DecodeInputs { } use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_onpair::onpair_compress; static SESSION: LazyLock = LazyLock::new(|| { @@ -166,10 +167,13 @@ fn compress(n: usize, shape: Shape, ctx: &mut ExecutionCtx) -> OnPairArray { strings.iter().map(|s| Some(s.as_bytes())), DType::Utf8(Nullability::NonNullable), ); - onpair_compress(varbin.as_array(), DEFAULT_CONFIG, ctx) + let array = onpair_compress(varbin.as_array(), DEFAULT_CONFIG, ctx) .unwrap_or_else(|e| panic!("onpair_compress failed: {e}")) .try_downcast::() - .unwrap_or_else(|array| panic!("expected OnPair array, got {}", array.encoding_id())) + .unwrap_or_else(|array| panic!("expected OnPair array, got {}", array.encoding_id())); + mark_optimized(array.as_array(), &SESSION).unwrap(); + + array } /// Canonicalise a slot child to the decoder's native primitive width. diff --git a/encodings/pco/benches/scalar.rs b/encodings/pco/benches/scalar.rs index f39266881b3..9a3da626cb0 100644 --- a/encodings/pco/benches/scalar.rs +++ b/encodings/pco/benches/scalar.rs @@ -15,6 +15,7 @@ use vortex_array::VortexSessionExecute; use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_error::VortexExpect; use vortex_pco::Pco; @@ -55,9 +56,12 @@ fn pco(nullable: bool) -> ArrayRef { validity, ); let mut ctx = SESSION.create_execution_ctx(); - Pco::from_primitive(input.as_view(), 8, 1024, &mut ctx) + let array = Pco::from_primitive(input.as_view(), 8, 1024, &mut ctx) .vortex_expect("PCO compression") - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).vortex_expect("mark the PCO array as optimized"); + + array } fn indices(count: usize, scattered: bool) -> Vec { diff --git a/encodings/sparse/benches/sparse_canonical.rs b/encodings/sparse/benches/sparse_canonical.rs index c16ab1c7883..4a852d3a4b2 100644 --- a/encodings/sparse/benches/sparse_canonical.rs +++ b/encodings/sparse/benches/sparse_canonical.rs @@ -24,6 +24,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_error::VortexExpect; @@ -82,7 +83,7 @@ fn make_sparse_list( ) .into_array(); - Sparse::try_new( + let array = Sparse::try_new( patch_indices.into_array(), patch_values, len, @@ -90,7 +91,10 @@ fn make_sparse_list( &mut ctx, ) .vortex_expect("sparse list input should be valid") - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).vortex_expect("mark the sparse list as optimized"); + + array } fn make_sparse_fixed_size_list(len: usize, patch_stride: usize, list_size: u32) -> ArrayRef { @@ -110,7 +114,7 @@ fn make_sparse_fixed_size_list(len: usize, patch_stride: usize, list_size: u32) NonNullable, ); - Sparse::try_new( + let array = Sparse::try_new( patch_indices.into_array(), patch_values, len, @@ -118,7 +122,10 @@ fn make_sparse_fixed_size_list(len: usize, patch_stride: usize, list_size: u32) &mut ctx, ) .vortex_expect("sparse fixed-size-list input should be valid") - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).vortex_expect("mark the sparse fixed-size list as optimized"); + + array } #[divan::bench(args = LIST_ARGS)] diff --git a/encodings/sparse/benches/sparse_pushdown.rs b/encodings/sparse/benches/sparse_pushdown.rs index 6d1c674feb5..3b436a72069 100644 --- a/encodings/sparse/benches/sparse_pushdown.rs +++ b/encodings/sparse/benches/sparse_pushdown.rs @@ -36,6 +36,7 @@ use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; use vortex_array::scalar_fn::fns::operators::Operator; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::Buffer; use vortex_error::VortexExpect; use vortex_session::VortexSession; @@ -73,7 +74,7 @@ fn make_sparse(num_patches: usize, constant: bool) -> ArrayRef { let values: Buffer = (0..num_patches) .map(|i| if constant { 1 } else { 2 + i as i32 }) .collect(); - Sparse::try_new( + let array = Sparse::try_new( indices.into_array(), values.into_array(), LEN, @@ -81,7 +82,10 @@ fn make_sparse(num_patches: usize, constant: bool) -> ArrayRef { &mut ctx, ) .vortex_expect("valid sparse") - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).vortex_expect("mark the sparse array as optimized"); + + array } /// Build a sparse `i32` array of `LEN` with a null fill and `num_patches` nullable patches @@ -95,7 +99,7 @@ fn make_sparse_nullable(num_patches: usize) -> ArrayRef { ) .into_array(); let nullable = DType::Primitive(PType::I32, Nullability::Nullable); - Sparse::try_new( + let array = Sparse::try_new( indices.into_array(), values, LEN, @@ -103,7 +107,10 @@ fn make_sparse_nullable(num_patches: usize) -> ArrayRef { &mut ctx, ) .vortex_expect("valid sparse") - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).vortex_expect("mark the sparse array as optimized"); + + array } #[divan::bench] diff --git a/vortex-array/benches/aggregate_grouped.rs b/vortex-array/benches/aggregate_grouped.rs index bd6e1c92936..8a7eb402f87 100644 --- a/vortex-array/benches/aggregate_grouped.rs +++ b/vortex-array/benches/aggregate_grouped.rs @@ -28,6 +28,7 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::VarBinViewArray; use vortex_array::dtype::DType; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -77,7 +78,7 @@ fn contiguous_list_view(elements: ArrayRef, group_sizes: &[usize]) -> ArrayRef { assert_eq!(elements.len(), total_element_count(group_sizes)); - ListViewArray::try_new( + let list_view = ListViewArray::try_new( elements, offsets.into_array(), sizes.into_array(), @@ -85,7 +86,10 @@ fn contiguous_list_view(elements: ArrayRef, group_sizes: &[usize]) -> ArrayRef { &mut ctx, ) .unwrap() - .into_array() + .into_array(); + mark_optimized(&list_view, &SESSION).unwrap(); + + list_view } fn i32_nullable_all_valid_input() -> ArrayRef { diff --git a/vortex-array/benches/aggregate_max.rs b/vortex-array/benches/aggregate_max.rs index dc3159e6428..5912a037096 100644 --- a/vortex-array/benches/aggregate_max.rs +++ b/vortex-array/benches/aggregate_max.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +#![expect(clippy::unwrap_used)] + use std::sync::LazyLock; use divan::Bencher; @@ -20,6 +22,7 @@ use vortex_array::dtype::NativeDecimalType; use vortex_array::dtype::Nullability; use vortex_array::dtype::i256; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -47,10 +50,10 @@ fn max_i32(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random::()).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_max::(ctx)); } @@ -61,10 +64,10 @@ fn max_i64(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random::()).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_max::(ctx)); } @@ -96,10 +99,11 @@ fn bench_decimal_min_max( bencher .with_inputs(|| { - ( - DecimalArray::new(values.clone(), decimal_dtype, validity.clone()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = + DecimalArray::new(values.clone(), decimal_dtype, validity.clone()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(array, ctx)| min_max(array, ctx, NumericalAggregateOpts::default())); } @@ -131,10 +135,10 @@ fn max_f64(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random::()).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_max::(ctx)); } @@ -154,10 +158,10 @@ fn max_i32_nulls_clustered(bencher: Bencher) { .collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_option_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_option_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_max::(ctx)); } @@ -171,10 +175,10 @@ fn max_i32_nulls_scattered(bencher: Bencher) { .collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_option_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_option_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_max::(ctx)); } @@ -194,14 +198,14 @@ const STRING_N: usize = 4_096; fn bench_min_max(bencher: Bencher, values: Vec>) { bencher .with_inputs(|| { - ( - VarBinViewArray::from_iter( - values.iter().map(|v| v.as_deref()), - DType::Utf8(Nullability::Nullable), - ) - .into_array(), - SESSION.create_execution_ctx(), + let array = VarBinViewArray::from_iter( + values.iter().map(|v| v.as_deref()), + DType::Utf8(Nullability::Nullable), ) + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| min_max(a, ctx, NumericalAggregateOpts::default())); } diff --git a/vortex-array/benches/aggregate_sum.rs b/vortex-array/benches/aggregate_sum.rs index fd7a7944ed4..50875aebe22 100644 --- a/vortex-array/benches/aggregate_sum.rs +++ b/vortex-array/benches/aggregate_sum.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +#![expect(clippy::unwrap_used)] + use std::sync::LazyLock; use divan::Bencher; @@ -13,6 +15,7 @@ use vortex_array::aggregate_fn::session::AggregateFnSession; use vortex_array::arrays::PrimitiveArray; use vortex_array::expr::stats::Stat; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -38,10 +41,10 @@ fn sum_i32(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random_range(-1000..1000)).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_as::(Stat::Sum, ctx)); } @@ -52,10 +55,10 @@ fn sum_v2_i32(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random_range(-1000..1000)).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| sum_v2(a, ctx)); } @@ -66,10 +69,10 @@ fn sum_u32(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random_range(0..2000)).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_as::(Stat::Sum, ctx)); } @@ -80,10 +83,10 @@ fn sum_v2_u32(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random_range(0..2000)).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| sum_v2(a, ctx)); } @@ -94,10 +97,10 @@ fn sum_i64(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random_range(-1000..1000)).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_as::(Stat::Sum, ctx)); } @@ -108,10 +111,10 @@ fn sum_v2_i64(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random_range(-1000..1000)).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| sum_v2(a, ctx)); } @@ -122,10 +125,10 @@ fn sum_f64(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random_range(-1000.0..1000.0)).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_as::(Stat::Sum, ctx)); } @@ -136,10 +139,10 @@ fn sum_v2_f64(bencher: Bencher) { let data: Vec = (0..N).map(|_| rng.random_range(-1000.0..1000.0)).collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| sum_v2(a, ctx)); } @@ -158,10 +161,10 @@ fn sum_f64_nulls_clustered(bencher: Bencher) { .collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_option_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_option_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_as::(Stat::Sum, ctx)); } @@ -180,10 +183,10 @@ fn sum_v2_f64_nulls_clustered(bencher: Bencher) { .collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_option_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_option_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| sum_v2(a, ctx)); } @@ -204,10 +207,10 @@ fn sum_i32_nulls_clustered(bencher: Bencher) { .collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_option_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_option_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_as::(Stat::Sum, ctx)); } @@ -226,10 +229,10 @@ fn sum_v2_i32_nulls_clustered(bencher: Bencher) { .collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_option_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_option_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| sum_v2(a, ctx)); } @@ -244,10 +247,10 @@ fn sum_i32_nulls_scattered(bencher: Bencher) { .collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_option_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_option_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| a.statistics().compute_as::(Stat::Sum, ctx)); } @@ -260,10 +263,10 @@ fn sum_v2_i32_nulls_scattered(bencher: Bencher) { .collect(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_option_iter(data.iter().copied()).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::from_option_iter(data.iter().copied()).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| sum_v2(a, ctx)); } diff --git a/vortex-array/benches/binary_ops.rs b/vortex-array/benches/binary_ops.rs index 75b1b6927ba..480c3e03583 100644 --- a/vortex-array/benches/binary_ops.rs +++ b/vortex-array/benches/binary_ops.rs @@ -37,6 +37,7 @@ use vortex_array::dtype::DecimalDType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -358,6 +359,9 @@ fn bench_binary( rhs: ArrayRef, operator: Operator, ) { + mark_optimized(&lhs, &SESSION).unwrap(); + mark_optimized(&rhs, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); let len = lhs.len(); diff --git a/vortex-array/benches/bool_zip.rs b/vortex-array/benches/bool_zip.rs index 40994da33d7..8b2c9edd66b 100644 --- a/vortex-array/benches/bool_zip.rs +++ b/vortex-array/benches/bool_zip.rs @@ -14,6 +14,7 @@ use vortex_array::VortexSessionExecute; use vortex_array::arrays::BoolArray; use vortex_array::builtins::ArrayBuiltins; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_mask::Mask; use vortex_session::VortexSession; @@ -57,13 +58,19 @@ fn nullable(bencher: Bencher) { } fn run(bencher: Bencher, if_true: ArrayRef, if_false: ArrayRef) { + mark_optimized(&if_true, &SESSION).unwrap(); + mark_optimized(&if_false, &SESSION).unwrap(); + let mask = mask(); bencher .with_inputs(|| { + let mask = mask.clone().into_array(); + mark_optimized(&mask, &SESSION).unwrap(); + ( if_true.clone(), if_false.clone(), - mask.clone().into_array(), + mask, SESSION.create_execution_ctx(), ) }) diff --git a/vortex-array/benches/cast_decimal.rs b/vortex-array/benches/cast_decimal.rs index 58122a586dd..da964c67bf8 100644 --- a/vortex-array/benches/cast_decimal.rs +++ b/vortex-array/benches/cast_decimal.rs @@ -29,6 +29,7 @@ use vortex_array::dtype::DecimalDType; use vortex_array::dtype::NativeDecimalType; use vortex_array::dtype::Nullability; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BufferMut; use vortex_session::VortexSession; @@ -78,6 +79,8 @@ fn decimal_array( /// Casts `array` to `target` and forces execution, the unit measured by every bench below. fn bench_cast(bencher: Bencher, array: ArrayRef, target: DType) { + mark_optimized(&array, &SESSION).unwrap(); + bencher .with_inputs(|| (array.clone(), SESSION.create_execution_ctx())) .bench_refs(|(a, ctx)| a.cast(target.clone()).unwrap().execute::(ctx)); diff --git a/vortex-array/benches/cast_primitive.rs b/vortex-array/benches/cast_primitive.rs index e79946844e1..72bf028dd9a 100644 --- a/vortex-array/benches/cast_primitive.rs +++ b/vortex-array/benches/cast_primitive.rs @@ -20,6 +20,7 @@ use vortex_array::dtype::Nullability; use vortex_array::dtype::PType; use vortex_array::expr::stats::Stat; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -49,6 +50,8 @@ fn cast_u16_to_u32(bencher: Bencher, n: usize) { rng.random_bool(0.5).then_some(i as u16) })) .into_array(); + mark_optimized(&arr, &SESSION).unwrap(); + // Pre-compute min/max so values_fit_in is a cache hit during the benchmark. arr.statistics() .compute_all(&[Stat::Min, Stat::Max], &mut SESSION.create_execution_ctx()) @@ -72,6 +75,8 @@ fn cast_u32_to_u8(bencher: Bencher, n: usize) { .then(|| rng.random_range(0..u8::MAX) as u32) })) .into_array(); + mark_optimized(&arr, &SESSION).unwrap(); + bencher .with_inputs(|| (arr.clone(), SESSION.create_execution_ctx())) .bench_refs(|(a, ctx)| { @@ -92,15 +97,15 @@ fn cast_i32_to_u32(bencher: Bencher, n: usize) { .into_data_parts(); bencher .with_inputs(|| { - ( - PrimitiveArray::from_buffer_handle( - parts.buffer.clone(), - parts.ptype, - parts.validity.clone(), - ) - .into_array(), - SESSION.create_execution_ctx(), + let array = PrimitiveArray::from_buffer_handle( + parts.buffer.clone(), + parts.ptype, + parts.validity.clone(), ) + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(a, ctx)| { a.cast(DType::Primitive(PType::U32, Nullability::Nullable)) @@ -115,6 +120,8 @@ fn cast_i32_to_u32(bencher: Bencher, n: usize) { fn cast_i64_to_decimal38_scale2(bencher: Bencher, n: usize) { let arr = PrimitiveArray::from_iter((0..n).map(|value| i64::try_from(value).unwrap())).into_array(); + mark_optimized(&arr, &SESSION).unwrap(); + bencher .with_inputs(|| (arr.clone(), SESSION.create_execution_ctx())) .bench_refs(|(a, ctx)| { @@ -132,6 +139,8 @@ fn cast_i64_to_decimal38_scale2(bencher: Bencher, n: usize) { fn cast_i32_to_decimal9_scale2(bencher: Bencher, n: usize) { let arr = PrimitiveArray::from_iter((0..n).map(|value| i32::try_from(value).unwrap())).into_array(); + mark_optimized(&arr, &SESSION).unwrap(); + bencher .with_inputs(|| (arr.clone(), SESSION.create_execution_ctx())) .bench_refs(|(a, ctx)| { @@ -149,6 +158,8 @@ fn cast_i32_to_decimal9_scale2(bencher: Bencher, n: usize) { fn cast_i32_to_decimal9_scale0(bencher: Bencher, n: usize) { let arr = PrimitiveArray::from_iter((0..n).map(|value| i32::try_from(value).unwrap())).into_array(); + mark_optimized(&arr, &SESSION).unwrap(); + bencher .with_inputs(|| (arr.clone(), SESSION.create_execution_ctx())) .bench_refs(|(a, ctx)| { diff --git a/vortex-array/benches/chunk_array_builder.rs b/vortex-array/benches/chunk_array_builder.rs index 92cb559022f..f111ad64dd2 100644 --- a/vortex-array/benches/chunk_array_builder.rs +++ b/vortex-array/benches/chunk_array_builder.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +#![expect(clippy::unwrap_used)] + use std::sync::LazyLock; use divan::Bencher; @@ -22,6 +24,7 @@ use vortex_array::builders::builder_with_capacity_in; use vortex_array::dtype::DType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_error::VortexExpect; use vortex_session::VortexSession; @@ -50,6 +53,7 @@ static SESSION: LazyLock = LazyLock::new(|| { #[divan::bench(args = BENCH_ARGS)] fn chunked_bool_canonical_into(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunk = make_bool_chunks(len, chunk_count); + mark_optimized(&chunk, &SESSION).unwrap(); bencher .with_inputs(|| (&chunk, SESSION.create_execution_ctx())) @@ -67,6 +71,7 @@ fn chunked_bool_canonical_into(bencher: Bencher, (len, chunk_count): (usize, usi #[divan::bench(args = if cfg!(codspeed) { &[(10, 100), (100, 50)] } else { BENCH_ARGS })] fn chunked_opt_bool_canonical_into(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunk = make_opt_bool_chunks(len, chunk_count); + mark_optimized(&chunk, &SESSION).unwrap(); bencher .with_inputs(|| (&chunk, SESSION.create_execution_ctx())) @@ -83,6 +88,7 @@ fn chunked_opt_bool_canonical_into(bencher: Bencher, (len, chunk_count): (usize, #[divan::bench(args = BENCH_ARGS)] fn chunked_opt_bool_into_canonical(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunk = make_opt_bool_chunks(len, chunk_count); + mark_optimized(&chunk, &SESSION).unwrap(); bencher .with_inputs(|| (&chunk, SESSION.create_execution_ctx())) @@ -92,6 +98,7 @@ fn chunked_opt_bool_into_canonical(bencher: Bencher, (len, chunk_count): (usize, #[divan::bench(args = BENCH_ARGS)] fn chunked_varbinview_canonical_into(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunks = make_string_chunks(false, len, chunk_count); + mark_optimized(&chunks, &SESSION).unwrap(); bencher .with_inputs(|| (&chunks, SESSION.create_execution_ctx())) @@ -111,6 +118,7 @@ fn chunked_varbinview_canonical_into(bencher: Bencher, (len, chunk_count): (usiz #[divan::bench(args = BENCH_ARGS)] fn chunked_varbinview_into_canonical(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunks = make_string_chunks(false, len, chunk_count); + mark_optimized(&chunks, &SESSION).unwrap(); bencher .with_inputs(|| (&chunks, SESSION.create_execution_ctx())) @@ -120,6 +128,7 @@ fn chunked_varbinview_into_canonical(bencher: Bencher, (len, chunk_count): (usiz #[divan::bench(args = BENCH_ARGS)] fn chunked_varbinview_opt_canonical_into(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunks = make_string_chunks(true, len, chunk_count); + mark_optimized(&chunks, &SESSION).unwrap(); bencher .with_inputs(|| (&chunks, SESSION.create_execution_ctx())) @@ -139,6 +148,7 @@ fn chunked_varbinview_opt_canonical_into(bencher: Bencher, (len, chunk_count): ( #[divan::bench(args = BENCH_ARGS)] fn chunked_varbinview_opt_into_canonical(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunks = make_string_chunks(true, len, chunk_count); + mark_optimized(&chunks, &SESSION).unwrap(); bencher .with_inputs(|| (&chunks, SESSION.create_execution_ctx())) @@ -155,6 +165,7 @@ const VARBIN_BENCH_ARGS: &[(usize, usize)] = &[ #[divan::bench(args = VARBIN_BENCH_ARGS)] fn chunked_varbin_to_varbinview_builder(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunks = make_varbin_chunks(false, len, chunk_count); + mark_optimized(&chunks, &SESSION).unwrap(); bencher .with_inputs(|| (&chunks, SESSION.create_execution_ctx())) @@ -174,6 +185,7 @@ fn chunked_varbin_to_varbinview_builder(bencher: Bencher, (len, chunk_count): (u #[divan::bench(args = VARBIN_BENCH_ARGS)] fn chunked_varbin_opt_to_varbinview_builder(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunks = make_varbin_chunks(true, len, chunk_count); + mark_optimized(&chunks, &SESSION).unwrap(); bencher .with_inputs(|| (&chunks, SESSION.create_execution_ctx())) @@ -193,6 +205,7 @@ fn chunked_varbin_opt_to_varbinview_builder(bencher: Bencher, (len, chunk_count) #[divan::bench(args = VARBIN_BENCH_ARGS)] fn chunked_varbin_into_canonical(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunks = make_varbin_chunks(false, len, chunk_count); + mark_optimized(&chunks, &SESSION).unwrap(); bencher .with_inputs(|| (&chunks, SESSION.create_execution_ctx())) @@ -202,6 +215,7 @@ fn chunked_varbin_into_canonical(bencher: Bencher, (len, chunk_count): (usize, u #[divan::bench(args = BENCH_ARGS)] fn chunked_constant_i32_append_to_builder(bencher: Bencher, (len, chunk_count): (usize, usize)) { let chunk = make_constant_i32_chunks(len, chunk_count); + mark_optimized(&chunk, &SESSION).unwrap(); bencher .with_inputs(|| (&chunk, SESSION.create_execution_ctx())) @@ -227,6 +241,7 @@ fn chunked_constant_utf8_append_to_builder( (value, len, chunk_count): (&str, usize, usize), ) { let chunk = make_constant_utf8_chunks(value, len, chunk_count); + mark_optimized(&chunk, &SESSION).unwrap(); bencher .with_inputs(|| (&chunk, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/chunked_dict_builder.rs b/vortex-array/benches/chunked_dict_builder.rs index bb2306e98d5..1b622462f53 100644 --- a/vortex-array/benches/chunked_dict_builder.rs +++ b/vortex-array/benches/chunked_dict_builder.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +#![expect(clippy::unwrap_used)] + use std::sync::LazyLock; use divan::Bencher; @@ -14,6 +16,7 @@ use vortex_array::builders::builder_with_capacity_in; use vortex_array::dtype::NativePType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_error::VortexExpect; use vortex_session::VortexSession; @@ -49,6 +52,7 @@ fn chunked_dict_primitive_canonical_into( StandardUniform: Distribution, { let chunk = gen_dict_primitive_chunks::(len, unique_values, chunk_count); + mark_optimized(&chunk, &SESSION).unwrap(); bencher .with_inputs(|| (&chunk, SESSION.create_execution_ctx())) @@ -70,6 +74,7 @@ fn chunked_dict_primitive_into_canonical( StandardUniform: Distribution, { let chunk = gen_dict_primitive_chunks::(len, unique_values, chunk_count); + mark_optimized(&chunk, &SESSION).unwrap(); bencher .with_inputs(|| (chunk.clone(), SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/chunked_fsl_canonicalize.rs b/vortex-array/benches/chunked_fsl_canonicalize.rs index aefb9f488ca..a7f17f3967d 100644 --- a/vortex-array/benches/chunked_fsl_canonicalize.rs +++ b/vortex-array/benches/chunked_fsl_canonicalize.rs @@ -20,6 +20,7 @@ use vortex_array::VortexSessionExecute; use vortex_array::arrays::ChunkedArray; use vortex_array::arrays::FixedSizeListArray; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -67,6 +68,7 @@ fn create_chunked_fsl(list_size: usize, num_chunks: usize) -> ChunkedArray { #[divan::bench(args = NUM_CHUNKS, consts = LIST_SIZES)] fn canonicalize(bencher: Bencher, num_chunks: usize) { let chunked = create_chunked_fsl(LIST_SIZE, num_chunks).into_array(); + mark_optimized(&chunked, &SESSION).unwrap(); bencher .with_inputs(|| (&chunked, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/compare.rs b/vortex-array/benches/compare.rs index ddf6343e9ab..3f7f65a7b35 100644 --- a/vortex-array/benches/compare.rs +++ b/vortex-array/benches/compare.rs @@ -47,6 +47,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -69,6 +70,9 @@ fn main() { const ARRAY_SIZE: usize = 8_192; fn bench_compare(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef, op: Operator) { + mark_optimized(&lhs, &SESSION).unwrap(); + mark_optimized(&rhs, &SESSION).unwrap(); + let len = lhs.len(); bencher diff --git a/vortex-array/benches/compare_sorted.rs b/vortex-array/benches/compare_sorted.rs index 28e76fc8fb4..3a384c7dd0d 100644 --- a/vortex-array/benches/compare_sorted.rs +++ b/vortex-array/benches/compare_sorted.rs @@ -23,6 +23,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -45,6 +46,9 @@ const LEN: u32 = 4_096; fn bench_compare(bencher: Bencher, lhs: ArrayRef, constant: Scalar, op: Operator) { let rhs = ConstantArray::new(constant, lhs.len()).into_array(); + mark_optimized(&lhs, &SESSION).unwrap(); + mark_optimized(&rhs, &SESSION).unwrap(); + bencher .counter(ItemsCount::new(lhs.len())) .with_inputs(|| (&lhs, &rhs, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/dict_compare.rs b/vortex-array/benches/dict_compare.rs index f35a6762c6e..e4b7a311ffc 100644 --- a/vortex-array/benches/dict_compare.rs +++ b/vortex-array/benches/dict_compare.rs @@ -25,6 +25,7 @@ use vortex_array::expr::root; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -63,6 +64,7 @@ fn bench_compare_primitive(bencher: divan::Bencher, (len, uniqueness): (usize, u let primitive_arr = gen_primitive_for_dict::(len, uniqueness); let dict = dict_encode(&primitive_arr.clone().into_array(), &mut ctx).unwrap(); let value = primitive_arr.as_slice::()[0]; + mark_optimized(dict.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) @@ -83,6 +85,7 @@ fn bench_compare_varbin(bencher: divan::Bencher, (len, uniqueness): (usize, usiz let dict = dict_encode(&varbin_arr.clone().into_array(), &mut ctx).unwrap(); let const_bytes = varbin_arr.bytes_at(0, &mut ctx); let value = unsafe { str::from_utf8_unchecked(const_bytes.as_slice()) }; + mark_optimized(dict.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) @@ -103,6 +106,7 @@ fn bench_compare_varbinview(bencher: divan::Bencher, (len, uniqueness): (usize, let dict = dict_encode(&varbinview_arr.clone().into_array(), &mut ctx).unwrap(); let const_bytes = varbinview_arr.bytes_at(0); let value = unsafe { str::from_utf8_unchecked(const_bytes.as_slice()) }; + mark_optimized(dict.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) @@ -148,6 +152,7 @@ fn bench_compare_sliced_dict_primitive( let dict = dict_encode(&primitive_arr.clone().into_array(), &mut ctx).unwrap(); let dict = dict.into_array().slice(0..codes_len).unwrap(); let value = primitive_arr.as_slice::()[0]; + mark_optimized(&dict, &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) @@ -171,6 +176,7 @@ fn bench_compare_sliced_dict_varbinview( let dict = dict.into_array().slice(0..codes_len).unwrap(); let const_bytes = varbin_arr.bytes_at(0, &mut ctx); let value = unsafe { str::from_utf8_unchecked(const_bytes.as_slice()) }; + mark_optimized(&dict, &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/dict_compress.rs b/vortex-array/benches/dict_compress.rs index 0e0c5988f25..a0aeac5f4b0 100644 --- a/vortex-array/benches/dict_compress.rs +++ b/vortex-array/benches/dict_compress.rs @@ -21,6 +21,7 @@ use vortex_array::builders::dict::dict_encode; use vortex_array::dtype::NativePType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -75,6 +76,7 @@ where StandardUniform: Distribution, { let primitive_arr = gen_primitive_for_dict::(len, unique_values).into_array(); + mark_optimized(&primitive_arr, &SESSION).unwrap(); bencher .with_inputs(|| (&primitive_arr, SESSION.create_execution_ctx())) @@ -84,6 +86,7 @@ where #[divan::bench(args = BENCH_ARGS)] fn encode_varbin(bencher: Bencher, (len, unique_values): (usize, usize)) { let varbin_arr = VarBinArray::from(gen_varbin_words(len, unique_values)).into_array(); + mark_optimized(&varbin_arr, &SESSION).unwrap(); bencher .with_inputs(|| (&varbin_arr, SESSION.create_execution_ctx())) @@ -94,6 +97,7 @@ fn encode_varbin(bencher: Bencher, (len, unique_values): (usize, usize)) { fn encode_varbinview(bencher: Bencher, (len, unique_values): (usize, usize)) { let varbinview_arr = VarBinViewArray::from_iter_str(gen_varbin_words(len, unique_values)).into_array(); + mark_optimized(&varbinview_arr, &SESSION).unwrap(); bencher .with_inputs(|| (&varbinview_arr, SESSION.create_execution_ctx())) @@ -110,6 +114,7 @@ where let dict = dict_encode(&primitive_arr, &mut SESSION.create_execution_ctx()) .unwrap() .into_array(); + mark_optimized(&dict, &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) @@ -122,6 +127,7 @@ fn decode_varbin(bencher: Bencher, (len, unique_values): (usize, usize)) { let dict = dict_encode(&varbin_arr, &mut SESSION.create_execution_ctx()) .unwrap() .into_array(); + mark_optimized(&dict, &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) @@ -135,6 +141,7 @@ fn decode_varbinview(bencher: Bencher, (len, unique_values): (usize, usize)) { let dict = dict_encode(&varbinview_arr, &mut SESSION.create_execution_ctx()) .unwrap() .into_array(); + mark_optimized(&dict, &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/dict_mask.rs b/vortex-array/benches/dict_mask.rs index 6218e096f7c..043129a8803 100644 --- a/vortex-array/benches/dict_mask.rs +++ b/vortex-array/benches/dict_mask.rs @@ -18,6 +18,7 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::builtins::ArrayBuiltins; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_mask::Mask; use vortex_session::VortexSession; @@ -76,6 +77,8 @@ fn bench_dict_mask(bencher: Bencher, (fraction_valid, fraction_masked): (f64, f6 let values = PrimitiveArray::from_option_iter([None, Some(42i32)]).into_array(); let array = DictArray::try_new(codes, values).unwrap().into_array(); let filter_mask = filter_mask(len, fraction_masked, &mut rng); + mark_optimized(&array, &SESSION).unwrap(); + bencher .with_inputs(|| (&array, &filter_mask, SESSION.create_execution_ctx())) .bench_refs(|(array, filter_mask, ctx)| { diff --git a/vortex-array/benches/dict_unreferenced_mask.rs b/vortex-array/benches/dict_unreferenced_mask.rs index f90349fa0da..7260eaa7084 100644 --- a/vortex-array/benches/dict_unreferenced_mask.rs +++ b/vortex-array/benches/dict_unreferenced_mask.rs @@ -16,6 +16,7 @@ use vortex_array::arrays::DictArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::dict::DictArrayExt; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; static SESSION: LazyLock = @@ -51,6 +52,7 @@ fn bench_many_codes_few_values(bencher: Bencher, num_values: i32) { .into_array(); let array = DictArray::try_new(codes, values).unwrap(); + mark_optimized(array.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&array, SESSION.create_execution_ctx())) @@ -84,6 +86,7 @@ fn bench_many_nulls(bencher: Bencher, fraction_valid: f64) { .into_array(); let array = DictArray::try_new(codes, values).unwrap(); + mark_optimized(array.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&array, SESSION.create_execution_ctx())) @@ -118,6 +121,7 @@ fn bench_sparse_coverage(bencher: Bencher, fraction_coverage: f64) { .into_array(); let array = DictArray::try_new(codes, values).unwrap(); + mark_optimized(array.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&array, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/expr/case_when_bench.rs b/vortex-array/benches/expr/case_when_bench.rs index eac7849b3af..6dd82b02869 100644 --- a/vortex-array/benches/expr/case_when_bench.rs +++ b/vortex-array/benches/expr/case_when_bench.rs @@ -36,6 +36,7 @@ use vortex_array::scalar_fn::ScalarFnVTableExt; use vortex_array::scalar_fn::fns::case_when::CaseWhen; use vortex_array::scalar_fn::fns::case_when::CaseWhenOptions; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -96,6 +97,8 @@ fn bench_branches(bencher: Bencher, run_length: usize, lhs: ArrayRef, rhs: Array ) .unwrap() .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + bencher .with_inputs(|| SESSION.create_execution_ctx()) .bench_refs(|ctx| array.clone().execute::(ctx).unwrap()); @@ -104,14 +107,17 @@ fn bench_branches(bencher: Bencher, run_length: usize, lhs: ArrayRef, rhs: Array fn make_struct_array(size: usize) -> ArrayRef { let data: Buffer = (0..size as i32).collect(); let field = data.into_array(); - StructArray::from_fields(&[("value", field)]) + let array = StructArray::from_fields(&[("value", field)]) .unwrap() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// Array with boolean columns cycling through thirds: `c0[i] = i%3==0`, `c1[i] = i%3==1`. fn make_fragmented_array(size: usize) -> ArrayRef { - StructArray::from_fields(&[ + let array = StructArray::from_fields(&[ ( "c0", BoolArray::from_iter((0..size).map(|i| i % 3 == 0)).into_array(), @@ -122,7 +128,10 @@ fn make_fragmented_array(size: usize) -> ArrayRef { ), ]) .unwrap() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// Benchmark a simple binary CASE WHEN with varying array sizes. diff --git a/vortex-array/benches/filter_bool.rs b/vortex-array/benches/filter_bool.rs index 7570f32a7d2..a71152cc9f6 100644 --- a/vortex-array/benches/filter_bool.rs +++ b/vortex-array/benches/filter_bool.rs @@ -21,6 +21,7 @@ use vortex_array::RecursiveCanonical; use vortex_array::VortexSessionExecute; use vortex_array::arrays::BoolArray; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::BitBuffer; use vortex_mask::Mask; use vortex_session::VortexSession; @@ -157,6 +158,8 @@ fn mask_rng() -> StdRng { #[divan::bench(args = SIZES)] fn filter_random_by_mostly_true(bencher: Bencher, n: usize) { let array = make_random_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -177,6 +180,8 @@ fn filter_random_by_mostly_true(bencher: Bencher, n: usize) { #[divan::bench(args = SIZES)] fn filter_random_by_mostly_false(bencher: Bencher, n: usize) { let array = make_random_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -197,6 +202,8 @@ fn filter_random_by_mostly_false(bencher: Bencher, n: usize) { #[divan::bench(args = SIZES)] fn filter_random_by_random(bencher: Bencher, n: usize) { let array = make_random_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -217,6 +224,8 @@ fn filter_random_by_random(bencher: Bencher, n: usize) { #[divan::bench(args = SIZES)] fn filter_random_by_correlated_runs(bencher: Bencher, n: usize) { let array = make_random_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -237,6 +246,8 @@ fn filter_random_by_correlated_runs(bencher: Bencher, n: usize) { #[divan::bench(args = SIZES)] fn filter_random_by_power_law(bencher: Bencher, n: usize) { let array = make_random_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -259,6 +270,8 @@ fn filter_random_by_power_law(bencher: Bencher, n: usize) { #[divan::bench(args = SIZES)] fn filter_powerlaw_by_mostly_true(bencher: Bencher, n: usize) { let array = make_power_law_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -279,6 +292,8 @@ fn filter_powerlaw_by_mostly_true(bencher: Bencher, n: usize) { #[divan::bench(args = SIZES)] fn filter_powerlaw_by_mostly_false(bencher: Bencher, n: usize) { let array = make_power_law_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -300,6 +315,8 @@ fn filter_powerlaw_by_mostly_false(bencher: Bencher, n: usize) { #[divan::bench(args = if cfg!(codspeed) { &[100_000] } else { SIZES })] fn filter_powerlaw_by_random(bencher: Bencher, n: usize) { let array = make_power_law_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -321,6 +338,8 @@ fn filter_powerlaw_by_random(bencher: Bencher, n: usize) { #[divan::bench(args = if cfg!(codspeed) { &[10_000] } else { SIZES })] fn filter_powerlaw_by_correlated_runs(bencher: Bencher, n: usize) { let array = make_power_law_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -341,6 +360,8 @@ fn filter_powerlaw_by_correlated_runs(bencher: Bencher, n: usize) { #[divan::bench(args = SIZES)] fn filter_powerlaw_by_power_law(bencher: Bencher, n: usize) { let array = make_power_law_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -367,6 +388,8 @@ const DENSITIES: &[f64] = &[ #[divan::bench(args = DENSITIES)] fn density_sweep_random(bencher: Bencher, density: f64) { let array = make_random_bool_array(DENSITY_SWEEP_SIZE, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -387,6 +410,8 @@ fn density_sweep_random(bencher: Bencher, density: f64) { #[divan::bench(args = DENSITIES)] fn density_sweep_dense_runs(bencher: Bencher, density: f64) { let array = make_random_bool_array(DENSITY_SWEEP_SIZE, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + let false_rate = (1.0 - density).max(0.0001); bencher .with_inputs(|| { @@ -408,6 +433,8 @@ fn density_sweep_dense_runs(bencher: Bencher, density: f64) { #[divan::bench] fn density_sweep_single_slice(bencher: Bencher) { let array = make_random_bool_array(DENSITY_SWEEP_SIZE, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -442,6 +469,8 @@ const ULTRA_SPARSE_SIZES: &[usize] = if cfg!(codspeed) { #[divan::bench(args = ALL_TRUE_SIZES)] fn filter_all_true(bencher: Bencher, n: usize) { let array = make_random_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( @@ -462,6 +491,8 @@ fn filter_all_true(bencher: Bencher, n: usize) { #[divan::bench(args = LARGE_SIZES)] fn filter_one_false(bencher: Bencher, n: usize) { let array = make_random_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { let mut bits: Vec = vec![true; n]; @@ -484,6 +515,8 @@ fn filter_one_false(bencher: Bencher, n: usize) { #[divan::bench(args = ULTRA_SPARSE_SIZES)] fn filter_ultra_sparse(bencher: Bencher, n: usize) { let array = make_random_bool_array(n, &mut StdRng::seed_from_u64(ARRAY_SEED)); + mark_optimized(array.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| { ( diff --git a/vortex-array/benches/filter_fixed_width.rs b/vortex-array/benches/filter_fixed_width.rs index b8799dfe558..01045216525 100644 --- a/vortex-array/benches/filter_fixed_width.rs +++ b/vortex-array/benches/filter_fixed_width.rs @@ -26,6 +26,7 @@ use vortex_array::dtype::DecimalDType; use vortex_array::dtype::i256; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::BitBuffer; use vortex_mask::Mask; use vortex_session::VortexSession; @@ -92,6 +93,8 @@ fn bench_filter( make_mask: impl Fn(usize) -> Mask + Sync, cache_indices: bool, ) { + mark_optimized(&array, &SESSION).unwrap(); + let len = array.len(); bencher .with_inputs(|| { diff --git a/vortex-array/benches/filter_fsl.rs b/vortex-array/benches/filter_fsl.rs index 86082ad43ba..0b18ea42c67 100644 --- a/vortex-array/benches/filter_fsl.rs +++ b/vortex-array/benches/filter_fsl.rs @@ -20,6 +20,7 @@ use vortex_array::arrays::FixedSizeListArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_mask::Mask; @@ -58,6 +59,7 @@ fn fsl_i32(bencher: Bencher, density: f64) { let elements = PrimitiveArray::from_iter(0..(len * LIST_SIZE as usize) as i32).into_array(); let array = FixedSizeListArray::new(elements, LIST_SIZE, Validity::NonNullable, len).into_array(); + mark_optimized(&array, &SESSION).unwrap(); let mut rng = StdRng::seed_from_u64(0); let bits = BitBuffer::from_iter((0..len).map(|_| rng.random_bool(density))); diff --git a/vortex-array/benches/filter_list.rs b/vortex-array/benches/filter_list.rs index 09fa775ce8f..ebc252570ed 100644 --- a/vortex-array/benches/filter_list.rs +++ b/vortex-array/benches/filter_list.rs @@ -18,6 +18,7 @@ use vortex_array::arrays::ListArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_mask::Mask; @@ -88,6 +89,8 @@ fn selection_mask(setup: MaskSetup) -> Mask { } fn run(bencher: Bencher, array: ArrayRef, mask: Mask) { + mark_optimized(&array, &SESSION).unwrap(); + bencher .with_inputs(|| (array.clone(), mask.clone(), SESSION.create_execution_ctx())) .bench_values(|(array, mask, mut ctx)| { diff --git a/vortex-array/benches/interleave.rs b/vortex-array/benches/interleave.rs index de96de7ab53..2b744c05fad 100644 --- a/vortex-array/benches/interleave.rs +++ b/vortex-array/benches/interleave.rs @@ -32,6 +32,7 @@ use vortex_array::arrays::InterleaveArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -190,16 +191,16 @@ fn vortex(bencher: Bencher, combo: Combo) { let (values, array_indices, row_indices) = vortex_inputs(combo); bencher .with_inputs(|| { - ( - InterleaveArray::try_new( - values.clone(), - array_indices.clone().into_array(), - row_indices.clone().into_array(), - ) - .unwrap() - .into_array(), - SESSION.create_execution_ctx(), + let array = InterleaveArray::try_new( + values.clone(), + array_indices.clone().into_array(), + row_indices.clone().into_array(), ) + .unwrap() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(array, ctx)| array.clone().execute::(ctx)); } @@ -208,16 +209,16 @@ fn bench_primitive(bencher: Bencher, combo: Combo, mixed_constants: bool) { let (values, array_indices, row_indices) = primitive_inputs(combo, mixed_constants); bencher .with_inputs(|| { - ( - InterleaveArray::try_new( - values.clone(), - array_indices.clone().into_array(), - row_indices.clone().into_array(), - ) - .unwrap() - .into_array(), - SESSION.create_execution_ctx(), + let array = InterleaveArray::try_new( + values.clone(), + array_indices.clone().into_array(), + row_indices.clone().into_array(), ) + .unwrap() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(array, ctx)| array.clone().execute::(ctx)); } diff --git a/vortex-array/benches/kleene_bool.rs b/vortex-array/benches/kleene_bool.rs index ddb54ae78c2..8eaf932809a 100644 --- a/vortex-array/benches/kleene_bool.rs +++ b/vortex-array/benches/kleene_bool.rs @@ -21,6 +21,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -181,6 +182,9 @@ fn or_null_constant_shifted(bencher: Bencher) { } fn bench_kleene(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef, operator: Operator) { + mark_optimized(&lhs, &SESSION).unwrap(); + mark_optimized(&rhs, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher.counter(ItemsCount::new(LEN)).bench_local(|| { @@ -193,6 +197,9 @@ fn bench_kleene(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef, operator: Operat } fn bench_kleene_constant_fold(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef, operator: Operator) { + mark_optimized(&lhs, &SESSION).unwrap(); + mark_optimized(&rhs, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher.bench_local(|| { diff --git a/vortex-array/benches/like.rs b/vortex-array/benches/like.rs index 7d7f29ed0bd..ab58ee5cba8 100644 --- a/vortex-array/benches/like.rs +++ b/vortex-array/benches/like.rs @@ -21,6 +21,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::fns::like::Like; use vortex_array::scalar_fn::fns::like::LikeOptions; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; static SESSION: LazyLock = LazyLock::new(|| { @@ -86,10 +87,10 @@ fn bench_like(bencher: Bencher, pattern: &str, options: LikeOptions) { let array = strings(); bencher .with_inputs(|| { - ( - like(array.clone(), pattern, options), - SESSION.create_execution_ctx(), - ) + let array = like(array.clone(), pattern, options); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); } @@ -127,12 +128,12 @@ fn like_per_row_patterns(bencher: Bencher) { let patterns = VarBinViewArray::from_iter_str((0..ARRAY_SIZE).map(|_| "hello%")).into_array(); bencher .with_inputs(|| { - ( - Like::try_new(array.clone(), patterns.clone(), LikeOptions::default()) - .unwrap() - .into_array(), - SESSION.create_execution_ctx(), - ) + let array = Like::try_new(array.clone(), patterns.clone(), LikeOptions::default()) + .unwrap() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); } diff --git a/vortex-array/benches/list_contains_set.rs b/vortex-array/benches/list_contains_set.rs index 56b486dd404..02fc50a3461 100644 --- a/vortex-array/benches/list_contains_set.rs +++ b/vortex-array/benches/list_contains_set.rs @@ -34,6 +34,7 @@ use vortex_array::expr::root; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; static SESSION: LazyLock = LazyLock::new(|| { @@ -82,10 +83,10 @@ fn bench_in_set(bencher: Bencher, set: Scalar, needles: ArrayRef) { .unwrap(); bencher .with_inputs(|| { - ( - needles.clone().apply_bound(&expr).unwrap(), - SESSION.create_execution_ctx(), - ) + let array = needles.clone().apply_bound(&expr).unwrap(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); } diff --git a/vortex-array/benches/list_length.rs b/vortex-array/benches/list_length.rs index 238f8ab3be2..8f9ce99d6cf 100644 --- a/vortex-array/benches/list_length.rs +++ b/vortex-array/benches/list_length.rs @@ -30,6 +30,7 @@ use vortex_array::expr::list_length; use vortex_array::expr::root; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -125,6 +126,8 @@ fn make_listview(num_lists: usize) -> ArrayRef { /// Apply `list_length(root())` and materialize the result. fn run(bencher: Bencher, array: ArrayRef) { + mark_optimized(&array, &SESSION).unwrap(); + let expr = list_length(root()); bencher .with_inputs(|| (&array, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/list_sum.rs b/vortex-array/benches/list_sum.rs index fe366820140..a000686aab3 100644 --- a/vortex-array/benches/list_sum.rs +++ b/vortex-array/benches/list_sum.rs @@ -29,6 +29,7 @@ use vortex_array::expr::list_sum; use vortex_array::expr::root; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -146,6 +147,8 @@ fn make_fsl(num_lists: usize) -> ArrayRef { /// Apply `list_sum(root())` and materialize the result. fn run(bencher: Bencher, array: ArrayRef) { + mark_optimized(&array, &SESSION).unwrap(); + let expr = list_sum(root()); bencher .with_inputs(|| (&array, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/listview_builder_extend.rs b/vortex-array/benches/listview_builder_extend.rs index a094a3aa506..8ba91fe8f0d 100644 --- a/vortex-array/benches/listview_builder_extend.rs +++ b/vortex-array/benches/listview_builder_extend.rs @@ -22,6 +22,7 @@ use vortex_array::dtype::Nullability::Nullable; use vortex_array::dtype::PType::I32; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -88,6 +89,7 @@ fn extend_from_array_zctl(bencher: Bencher, (num_lists, list_size): (usize, usiz make_listview(num_lists, list_size, list_size, false).with_zero_copy_to_list(true) }; let source = source.into_array(); + mark_optimized(&source, &SESSION).unwrap(); bencher.with_inputs(|| &source).bench_refs(|source| { let mut ctx = SESSION.create_execution_ctx(); @@ -112,6 +114,7 @@ fn extend_from_array_non_zctl_overlapping( let source = make_listview(num_lists, list_size, 1, true); debug_assert!(!source.is_zero_copy_to_list()); let source = source.into_array(); + mark_optimized(&source, &SESSION).unwrap(); bencher.with_inputs(|| &source).bench_refs(|source| { let mut ctx = SESSION.create_execution_ctx(); diff --git a/vortex-array/benches/listview_rebuild.rs b/vortex-array/benches/listview_rebuild.rs index 1a08600f4cf..4e1abb7c176 100644 --- a/vortex-array/benches/listview_rebuild.rs +++ b/vortex-array/benches/listview_rebuild.rs @@ -25,6 +25,7 @@ use vortex_array::arrays::listview::ListViewRebuildMode; use vortex_array::dtype::FieldNames; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -141,6 +142,8 @@ fn make_nested_list_lv( #[divan::bench] fn i32_small(bencher: Bencher) { let lv = make_primitive_lv(50, 32, 32); + mark_optimized(lv.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| (&lv, SESSION.create_execution_ctx())) .bench_refs(|(lv, ctx)| { @@ -158,6 +161,8 @@ fn i32_small(bencher: Bencher) { #[divan::bench] fn i32_small_overlapping(bencher: Bencher) { let lv = make_primitive_lv(50, 8, 1); + mark_optimized(lv.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| (&lv, SESSION.create_execution_ctx())) .bench_refs(|(lv, ctx)| { @@ -175,6 +180,8 @@ fn i32_small_overlapping(bencher: Bencher) { #[divan::bench] fn varbinview_small(bencher: Bencher) { let lv = make_varbinview_lv(50, 32, 32); + mark_optimized(lv.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| (&lv, SESSION.create_execution_ctx())) .bench_refs(|(lv, ctx)| { @@ -192,6 +199,8 @@ fn varbinview_small(bencher: Bencher) { #[divan::bench] fn struct_small(bencher: Bencher) { let lv = make_struct_lv(50, 32, 32); + mark_optimized(lv.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| (&lv, SESSION.create_execution_ctx())) .bench_refs(|(lv, ctx)| { @@ -209,6 +218,8 @@ fn struct_small(bencher: Bencher) { #[divan::bench] fn i32_large(bencher: Bencher) { let lv = make_primitive_lv(50, 1_024, 1_024); + mark_optimized(lv.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| (&lv, SESSION.create_execution_ctx())) .bench_refs(|(lv, ctx)| { @@ -226,6 +237,8 @@ fn i32_large(bencher: Bencher) { #[divan::bench] fn varbinview_large(bencher: Bencher) { let lv = make_varbinview_lv(4, 512, 512); + mark_optimized(lv.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| (&lv, SESSION.create_execution_ctx())) .bench_refs(|(lv, ctx)| { @@ -243,6 +256,8 @@ fn varbinview_large(bencher: Bencher) { #[divan::bench] fn struct_large(bencher: Bencher) { let lv = make_struct_lv(25, 1_024, 1_024); + mark_optimized(lv.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| (&lv, SESSION.create_execution_ctx())) .bench_refs(|(lv, ctx)| { @@ -275,6 +290,8 @@ fn fsl_large(bencher: Bencher) { Validity::NonNullable, &mut ctx, ); + mark_optimized(lv.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| (&lv, SESSION.create_execution_ctx())) .bench_refs(|(lv, ctx)| { @@ -292,6 +309,8 @@ fn fsl_large(bencher: Bencher) { #[divan::bench] fn list_i32_large(bencher: Bencher) { let lv = make_nested_list_lv(2, 512, 2); + mark_optimized(lv.as_array(), &SESSION).unwrap(); + bencher .with_inputs(|| (&lv, SESSION.create_execution_ctx())) .bench_refs(|(lv, ctx)| { diff --git a/vortex-array/benches/listview_zip.rs b/vortex-array/benches/listview_zip.rs index eafde33ea8e..36a44bec290 100644 --- a/vortex-array/benches/listview_zip.rs +++ b/vortex-array/benches/listview_zip.rs @@ -16,6 +16,7 @@ use vortex_array::arrays::ListViewArray; use vortex_array::builtins::ArrayBuiltins; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BufferMut; use vortex_mask::Mask; @@ -58,13 +59,19 @@ fn nullable(bencher: Bencher) { } fn run(bencher: Bencher, if_true: ArrayRef, if_false: ArrayRef) { + mark_optimized(&if_true, &SESSION).unwrap(); + mark_optimized(&if_false, &SESSION).unwrap(); + let mask = mask(); bencher .with_inputs(|| { + let mask = mask.clone().into_array(); + mark_optimized(&mask, &SESSION).unwrap(); + ( if_true.clone(), if_false.clone(), - mask.clone().into_array(), + mask, SESSION.create_execution_ctx(), ) }) diff --git a/vortex-array/benches/patches_lookup.rs b/vortex-array/benches/patches_lookup.rs index 71e569558b1..3d78c3a883a 100644 --- a/vortex-array/benches/patches_lookup.rs +++ b/vortex-array/benches/patches_lookup.rs @@ -17,6 +17,7 @@ use vortex_array::aggregate_fn::session::AggregateFnSession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::patches::PATCH_CHUNK_SIZE; use vortex_array::patches::Patches; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -118,6 +119,11 @@ fn queries_full_range() -> Vec { } fn bench_search_index(bencher: Bencher, patches: Patches, queries: Vec) { + mark_optimized(patches.indices(), &SESSION).unwrap(); + if let Some(chunk_offsets) = patches.chunk_offsets() { + mark_optimized(chunk_offsets, &SESSION).unwrap(); + } + bencher .with_inputs(|| (&patches, &queries, SESSION.create_execution_ctx())) .bench_refs(|(patches, queries, ctx)| { diff --git a/vortex-array/benches/piecewise_sequence_take_primitive.rs b/vortex-array/benches/piecewise_sequence_take_primitive.rs index 1a17757d3cb..317a3af08d0 100644 --- a/vortex-array/benches/piecewise_sequence_take_primitive.rs +++ b/vortex-array/benches/piecewise_sequence_take_primitive.rs @@ -21,6 +21,7 @@ use vortex_array::arrays::PiecewiseSequenceArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -49,6 +50,8 @@ fn optimized_constant_lengths(bencher: Bencher, run_length: usize) { run_length, lengths_constant(run_length, run_count(run_length)), ); + mark_optimized(&values, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); bencher .counter(BytesCount::of_many::(OUTPUT_LEN)) @@ -69,6 +72,8 @@ fn optimized_constant_lengths(bencher: Bencher, run_length: usize) { fn prebuilt_dense_lengths(bencher: Bencher, run_length: usize) { let values = values(); let indices = piecewise_indices(run_length, lengths_dense(run_length, run_count(run_length))); + mark_optimized(&values, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); bencher .counter(BytesCount::of_many::(OUTPUT_LEN)) diff --git a/vortex-array/benches/primitive_zip.rs b/vortex-array/benches/primitive_zip.rs index 0df9a15c6b5..3afa4a8822e 100644 --- a/vortex-array/benches/primitive_zip.rs +++ b/vortex-array/benches/primitive_zip.rs @@ -18,6 +18,7 @@ use vortex_array::VortexSessionExecute; use vortex_array::arrays::PrimitiveArray; use vortex_array::builtins::ArrayBuiltins; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BufferMut; use vortex_mask::Mask; @@ -59,13 +60,17 @@ fn nullable(bencher: Bencher) { } fn run(bencher: Bencher, if_true: ArrayRef, if_false: ArrayRef) { - let mask = mask(); + let mask = mask().into_array(); + mark_optimized(&if_true, &SESSION).unwrap(); + mark_optimized(&if_false, &SESSION).unwrap(); + mark_optimized(&mask, &SESSION).unwrap(); + bencher .with_inputs(|| { ( if_true.clone(), if_false.clone(), - mask.clone().into_array(), + mask.clone(), SESSION.create_execution_ctx(), ) }) diff --git a/vortex-array/benches/probe.rs b/vortex-array/benches/probe.rs index df2e8c4c16d..aaf15a362d5 100644 --- a/vortex-array/benches/probe.rs +++ b/vortex-array/benches/probe.rs @@ -17,6 +17,7 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::StructArray; use vortex_array::dtype::FieldNames; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_error::VortexExpect; @@ -93,6 +94,7 @@ fn array(name: &str) -> ArrayRef { #[divan::bench(args = ARRAYS)] fn once(bencher: Bencher, name: &str) { let array = array(name); + mark_optimized(&array, &SESSION).vortex_expect("optimize the benchmark input"); let indices = indices(); bencher .with_inputs(|| SESSION.create_execution_ctx()) @@ -110,6 +112,7 @@ fn once(bencher: Bencher, name: &str) { #[divan::bench(args = ARRAYS)] fn repeated(bencher: Bencher, name: &str) { let array = array(name); + mark_optimized(&array, &SESSION).vortex_expect("optimize the benchmark input"); let indices = indices(); bencher .with_inputs(|| (SESSION.create_execution_ctx(), array.repeated_probe())) diff --git a/vortex-array/benches/row_fn_bool_retry.rs b/vortex-array/benches/row_fn_bool_retry.rs index 858c7eb8a3a..bf541842401 100644 --- a/vortex-array/benches/row_fn_bool_retry.rs +++ b/vortex-array/benches/row_fn_bool_retry.rs @@ -26,6 +26,7 @@ use vortex_array::scalar_fn::VecExecutionArgs; use vortex_array::scalar_fn::unstable::row::RowFn; use vortex_array::scalar_fn::unstable::row::RowVisitor; use vortex_array::scalar_fn::unstable::row::execute_rows; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_error::VortexExpect; use vortex_error::VortexResult; @@ -128,6 +129,9 @@ fn deferred_bool(bencher: Bencher, &(shape, scenario): &(Inpu ), InputShape::ConstantLhs => (ConstantArray::new(512_i64, ROWS).into_array(), column), }; + mark_optimized(&lhs, &SESSION).vortex_expect("optimize the benchmark input"); + mark_optimized(&rhs, &SESSION).vortex_expect("optimize the benchmark input"); + let args = VecExecutionArgs::new(vec![lhs, rhs], ROWS); let function = Predicate; drop( diff --git a/vortex-array/benches/row_fn_output.rs b/vortex-array/benches/row_fn_output.rs index 200a04a7810..c81ee01b81b 100644 --- a/vortex-array/benches/row_fn_output.rs +++ b/vortex-array/benches/row_fn_output.rs @@ -27,6 +27,7 @@ use vortex_array::scalar_fn::VecExecutionArgs; use vortex_array::scalar_fn::unstable::row::RowFn; use vortex_array::scalar_fn::unstable::row::RowVisitor; use vortex_array::scalar_fn::unstable::row::execute_rows; +use vortex_array::test_harness::mark_optimized; use vortex_error::VortexExpect; use vortex_error::VortexResult; use vortex_error::vortex_ensure; @@ -227,6 +228,10 @@ fn make_args(shape: InputShape) -> VecExecutionArgs { InputShape::ConstantPerRow => vec![T::constant(), T::per_row(1)], }; + for arg in &args { + mark_optimized(arg, &SESSION).vortex_expect("optimize the benchmark input"); + } + VecExecutionArgs::new(args, ROWS) } diff --git a/vortex-array/benches/row_fn_utf8_input.rs b/vortex-array/benches/row_fn_utf8_input.rs index cd559d3fb60..344344c9d7d 100644 --- a/vortex-array/benches/row_fn_utf8_input.rs +++ b/vortex-array/benches/row_fn_utf8_input.rs @@ -29,6 +29,7 @@ use vortex_array::scalar_fn::unstable::row::RowFn; use vortex_array::scalar_fn::unstable::row::RowVisitor; use vortex_array::scalar_fn::unstable::row::Utf8Column; use vortex_array::scalar_fn::unstable::row::execute_rows; +use vortex_array::test_harness::mark_optimized; use vortex_error::VortexExpect; use vortex_error::VortexResult; use vortex_session::VortexSession; @@ -140,7 +141,9 @@ fn make_input(layout: Layout, len: usize) -> ArrayRef { #[vortex_bench_support::cpu_features] #[divan::bench(args = cases())] fn starts_with(bencher: Bencher, case: Case) { - let args = VecExecutionArgs::new(vec![make_input(case.layout, case.len)], ROWS); + let input = make_input(case.layout, case.len); + mark_optimized(&input, &SESSION).vortex_expect("optimize the benchmark input"); + let args = VecExecutionArgs::new(vec![input], ROWS); bencher .counter(ItemsCount::new(ROWS)) diff --git a/vortex-array/benches/run_end_compress.rs b/vortex-array/benches/run_end_compress.rs index 71594450532..781bf8a08f6 100644 --- a/vortex-array/benches/run_end_compress.rs +++ b/vortex-array/benches/run_end_compress.rs @@ -20,6 +20,7 @@ use vortex_array::dtype::IntegerPType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -79,6 +80,7 @@ fn compress(bencher: Bencher, (length, run_step): (usize, usize)) { .collect::>(), Validity::NonNullable, ); + mark_optimized(values.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&values, SESSION.create_execution_ctx())) @@ -100,6 +102,7 @@ fn decompress(bencher: Bencher, (length, run_step): (usize, usi let run_end_array = RunEnd::new(ends, values, &mut SESSION.create_execution_ctx()); let array = run_end_array.into_array(); + mark_optimized(&array, &SESSION).unwrap(); bencher .with_inputs(|| (array.clone(), SESSION.create_execution_ctx())) @@ -127,6 +130,8 @@ fn take_indices(bencher: Bencher, (length, run_step): (usize, usize)) { let runend_array = RunEnd::try_new(ends.into_array(), values, &mut encode_ctx) .unwrap() .into_array(); + mark_optimized(&source_array, &SESSION).unwrap(); + mark_optimized(&runend_array, &SESSION).unwrap(); bencher .with_inputs(|| (&source_array, &runend_array, SESSION.create_execution_ctx())) @@ -152,6 +157,7 @@ fn decompress_utf8(bencher: Bencher, (length, run_step): (usize, usize)) { let run_end_array = RunEnd::new(ends, values, &mut SESSION.create_execution_ctx()); let array = run_end_array.into_array(); + mark_optimized(&array, &SESSION).unwrap(); bencher .with_inputs(|| (array.clone(), SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/run_end_decode.rs b/vortex-array/benches/run_end_decode.rs index 53577cdc158..241fa23034a 100644 --- a/vortex-array/benches/run_end_decode.rs +++ b/vortex-array/benches/run_end_decode.rs @@ -15,9 +15,11 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::runend::decompress_bool::runend_decode_bools; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_buffer::BufferMut; +use vortex_error::VortexExpect; use vortex_session::VortexSession; #[global_allocator] @@ -225,6 +227,9 @@ fn decode_bool(bencher: Bencher, args: BoolBenchArgs) { distribution, } = args; let (ends, values) = create_bool_test_data(total_length, avg_run_length, distribution); + mark_optimized(ends.as_array(), &SESSION).vortex_expect("optimize the benchmark input"); + mark_optimized(values.as_array(), &SESSION).vortex_expect("optimize the benchmark input"); + bencher .with_inputs(|| (ends.clone(), values.clone(), SESSION.create_execution_ctx())) .bench_refs(|(ends, values, ctx)| { @@ -388,6 +393,9 @@ fn decode_bool_nullable(bencher: Bencher, args: NullableBoolBenchArgs) { } = args; let (ends, values) = create_nullable_bool_test_data(total_length, avg_run_length, distribution, validity); + mark_optimized(ends.as_array(), &SESSION).vortex_expect("optimize the benchmark input"); + mark_optimized(values.as_array(), &SESSION).vortex_expect("optimize the benchmark input"); + bencher .with_inputs(|| (ends.clone(), values.clone(), SESSION.create_execution_ctx())) .bench_refs(|(ends, values, ctx)| { diff --git a/vortex-array/benches/run_end_decode_primitive.rs b/vortex-array/benches/run_end_decode_primitive.rs index 3cb08048124..63f76cc37b9 100644 --- a/vortex-array/benches/run_end_decode_primitive.rs +++ b/vortex-array/benches/run_end_decode_primitive.rs @@ -20,9 +20,11 @@ use vortex_array::arrays::runend::compress::runend_decode_primitive; use vortex_array::dtype::NativePType; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_buffer::Buffer; +use vortex_error::VortexExpect; use vortex_session::VortexSession; #[global_allocator] @@ -74,6 +76,9 @@ fn bench_decode( Validity::NonNullable }; let values = PrimitiveArray::new(Buffer::from(values), validity); + mark_optimized(ends.as_array(), &SESSION).vortex_expect("optimize the benchmark input"); + mark_optimized(values.as_array(), &SESSION).vortex_expect("optimize the benchmark input"); + bencher .with_inputs(|| (ends.clone(), values.clone(), SESSION.create_execution_ctx())) .bench_refs(|(ends, values, ctx)| { diff --git a/vortex-array/benches/run_end_null_count.rs b/vortex-array/benches/run_end_null_count.rs index 25fc694e4d2..22119978984 100644 --- a/vortex-array/benches/run_end_null_count.rs +++ b/vortex-array/benches/run_end_null_count.rs @@ -18,6 +18,7 @@ use vortex_array::arrays::runend::RunEnd; use vortex_array::arrays::runend::RunEndArray; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -68,10 +69,10 @@ static SESSION: LazyLock = LazyLock::new(|| { fn null_count_run_end(bencher: Bencher, (n, run_step, valid_density): (usize, usize, f64)) { bencher .with_inputs(|| { - ( - fixture(n, run_step, valid_density).into_array(), - SESSION.create_execution_ctx(), - ) + let array = fixture(n, run_step, valid_density).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(array, ctx)| array.invalid_count(ctx).unwrap()); } diff --git a/vortex-array/benches/run_end_sum.rs b/vortex-array/benches/run_end_sum.rs index 280ae89d5f9..63d47ea3107 100644 --- a/vortex-array/benches/run_end_sum.rs +++ b/vortex-array/benches/run_end_sum.rs @@ -16,6 +16,7 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::runend::RunEnd; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; const LEN: usize = 65_536; @@ -55,6 +56,8 @@ fn runend_with_null_runs() -> ArrayRef { #[divan::bench] fn whole_array_sum_partially_valid(bencher: Bencher) { let array = runend_with_null_runs(); + mark_optimized(&array, &SESSION).unwrap(); + bencher .with_inputs(|| SESSION.create_execution_ctx()) .bench_refs(|ctx| sum_v2(&array, ctx).unwrap()); diff --git a/vortex-array/benches/run_end_take.rs b/vortex-array/benches/run_end_take.rs index 81e7335a3b9..3212984005e 100644 --- a/vortex-array/benches/run_end_take.rs +++ b/vortex-array/benches/run_end_take.rs @@ -21,6 +21,7 @@ use vortex_array::arrays::runend::RunEnd; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -127,6 +128,8 @@ const BENCH_ARGS: &[TakeBenchArgs] = &[ fn take(bencher: Bencher, args: TakeBenchArgs) { let array = run_end_array(args.array_len, args.run_step); let indices = take_indices(args); + mark_optimized(&array, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); bencher .with_inputs(|| (&array, &indices, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/scalar_at_struct.rs b/vortex-array/benches/scalar_at_struct.rs index 863fabf1884..1b5b83d3591 100644 --- a/vortex-array/benches/scalar_at_struct.rs +++ b/vortex-array/benches/scalar_at_struct.rs @@ -17,6 +17,7 @@ use vortex_array::VortexSessionExecute; use vortex_array::arrays::StructArray; use vortex_array::dtype::FieldNames; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -55,6 +56,7 @@ fn execute_scalar_struct_simple(bencher: Bencher) { ) .unwrap(); let struct_array: ArrayRef = struct_array.into_array(); + mark_optimized(&struct_array, &SESSION).unwrap(); let indices: Vec = (0..NUM_ACCESSES) .map(|_| rng.random_range(0..ARRAY_SIZE)) @@ -92,6 +94,7 @@ fn execute_scalar_struct_wide(bencher: Bencher) { StructArray::try_new(field_names, fields, ARRAY_SIZE, Validity::NonNullable) .unwrap() .into_array(); + mark_optimized(&struct_array, &SESSION).unwrap(); let indices: Vec = (0..NUM_ACCESSES) .map(|_| rng.random_range(0..ARRAY_SIZE)) diff --git a/vortex-array/benches/scalar_fn_probe.rs b/vortex-array/benches/scalar_fn_probe.rs index 4ce01520d17..1102a1f6248 100644 --- a/vortex-array/benches/scalar_fn_probe.rs +++ b/vortex-array/benches/scalar_fn_probe.rs @@ -22,6 +22,7 @@ use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::TypedScalarFnInstance; use vortex_array::scalar_fn::fns::binary::Binary; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -48,9 +49,12 @@ fn binary_add() -> ArrayRef { .into_array(); let rhs = PrimitiveArray::from_iter((0..ARRAY_SIZE).map(|i| i as i64)).into_array(); let scalar_fn = TypedScalarFnInstance::new(Binary, Operator::Add).erased(); - ScalarFnArray::try_new(scalar_fn, vec![lhs, rhs]) + let array = ScalarFnArray::try_new(scalar_fn, vec![lhs, rhs]) .unwrap() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// evaluating AND's validity is equal to evaluating the AND due to @@ -60,9 +64,12 @@ fn binary_and() -> ArrayRef { .into_array(); let rhs = BoolArray::from_iter((0..ARRAY_SIZE).map(|i| i % 2 == 0)).into_array(); let scalar_fn = TypedScalarFnInstance::new(Binary, Operator::And).erased(); - ScalarFnArray::try_new(scalar_fn, vec![lhs, rhs]) + let array = ScalarFnArray::try_new(scalar_fn, vec![lhs, rhs]) .unwrap() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } fn indices() -> Vec { diff --git a/vortex-array/benches/scalar_subtract.rs b/vortex-array/benches/scalar_subtract.rs index c882fc9fb76..17f5e1279c5 100644 --- a/vortex-array/benches/scalar_subtract.rs +++ b/vortex-array/benches/scalar_subtract.rs @@ -21,6 +21,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -57,6 +58,7 @@ fn scalar_subtract(bencher: Bencher) { let to_subtract = -1i64; let chunked = ChunkedArray::from_iter([data1, data2]).into_array(); + mark_optimized(&chunked, &SESSION).unwrap(); bencher .with_inputs(|| (&chunked, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/take_chunked.rs b/vortex-array/benches/take_chunked.rs index 6932d8d1136..592a495c5ed 100644 --- a/vortex-array/benches/take_chunked.rs +++ b/vortex-array/benches/take_chunked.rs @@ -24,6 +24,7 @@ use vortex_array::arrays::StructArray; use vortex_array::dtype::FieldNames; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -354,6 +355,8 @@ fn take_indices(case: Case) -> ArrayRef { fn take(bencher: Bencher, case: Case) { let values = chunked_values(case); let indices = take_indices(case); + mark_optimized(&values, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); bencher .counter(ItemsCount::new(case.indices)) diff --git a/vortex-array/benches/take_filter.rs b/vortex-array/benches/take_filter.rs index 021a3f6abfc..c5450ad0d2e 100644 --- a/vortex-array/benches/take_filter.rs +++ b/vortex-array/benches/take_filter.rs @@ -35,6 +35,7 @@ use vortex_array::arrays::ListArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_buffer::BitBufferMut; @@ -134,6 +135,8 @@ fn nullable_random_indices(num_indices: usize, filtered_len: usize) -> ArrayRef } fn bench_take_filter(bencher: Bencher, array: ArrayRef, indices: ArrayRef) { + mark_optimized(&indices, &SESSION).unwrap(); + bencher .with_inputs(|| (&array, &indices, SESSION.create_execution_ctx())) .bench_refs(|(array, indices, ctx)| { @@ -151,12 +154,16 @@ fn bench_take_filter_uncached_mask( mask: BitBuffer, indices: ArrayRef, ) { + mark_optimized(&indices, &SESSION).unwrap(); + bencher .with_inputs(|| { let array = child .clone() .filter(Mask::from_buffer(mask.clone())) .unwrap(); + mark_optimized(&array, &SESSION).unwrap(); + (array, indices.clone(), SESSION.create_execution_ctx()) }) .bench_values(|(array, indices, mut ctx)| { @@ -198,6 +205,8 @@ fn take_filter_primitive_random_mask_sequential_indices( let array = primitive_array() .filter(random_mask(ARRAY_LEN, FILTERED_LEN)) .unwrap(); + mark_optimized(&array, &SESSION).unwrap(); + bench_take_filter(bencher, array, random_indices(num_indices, FILTERED_LEN)); } @@ -220,6 +231,8 @@ fn take_filter_primitive_small_random_mask_random_indices( let array = list_array(NESTED_ARRAY_LEN) .filter(random_mask(NESTED_ARRAY_LEN, FILTERED_LEN)) .unwrap(); + mark_optimized(&array, &SESSION).unwrap(); + bench_take_filter(bencher, array, sequential_indices(num_indices)); } @@ -283,6 +298,8 @@ fn take_filter_list_random_mask_random_indices( let array = list_array(NESTED_ARRAY_LEN) .filter(random_mask(NESTED_ARRAY_LEN, FILTERED_LEN)) .unwrap(); + mark_optimized(&array, &SESSION).unwrap(); + bench_take_filter(bencher, array, random_indices(num_indices, FILTERED_LEN)); } @@ -294,6 +311,8 @@ fn take_filter_list_small_random_mask_random_indices( let array = list_array(NESTED_ARRAY_LEN) .filter(random_mask(NESTED_ARRAY_LEN, FILTERED_LEN)) .unwrap(); + mark_optimized(&array, &SESSION).unwrap(); + bench_take_filter(bencher, array, random_indices(num_indices, FILTERED_LEN)); } @@ -333,6 +352,8 @@ fn take_filter_primitive_nullable_random_mask_random_indices( indices.sort_unstable(); } let indices_array = Buffer::from(indices).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + mark_optimized(&indices_array, &SESSION).unwrap(); bencher .counter(BytesCount::of_many::(num_indices * LIST_SIZE)) diff --git a/vortex-array/benches/take_patches.rs b/vortex-array/benches/take_patches.rs index ecc4e330593..1688f505547 100644 --- a/vortex-array/benches/take_patches.rs +++ b/vortex-array/benches/take_patches.rs @@ -19,6 +19,7 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::patches::Patches; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::Buffer; use vortex_error::VortexExpect; use vortex_session::VortexSession; @@ -128,7 +129,10 @@ fn fixture(len: usize, sparsity: f64, rng: &mut StdRng) -> Patches { .collect::>(); let sparse_len = indices.len(); let values = Buffer::from_iter((0..sparse_len).map(|x| x as u64)).into_array(); - Patches::new(len, 0, indices.into_array(), values, None, &mut ctx).unwrap() + let patches = Patches::new(len, 0, indices.into_array(), values, None, &mut ctx).unwrap(); + mark_patches_optimized(&patches); + + patches } fn fixture_with_chunk_offsets(len: usize, sparsity: f64, rng: &mut StdRng) -> Patches { @@ -149,7 +153,7 @@ fn fixture_with_chunk_offsets(len: usize, sparsity: f64, rng: &mut StdRng) -> Pa }) .collect(); - Patches::new( + let patches = Patches::new( len, 0, Buffer::from(patch_indices).into_array(), @@ -157,9 +161,25 @@ fn fixture_with_chunk_offsets(len: usize, sparsity: f64, rng: &mut StdRng) -> Pa Some(Buffer::from(chunk_offsets).into_array()), &mut ctx, ) - .unwrap() + .unwrap(); + mark_patches_optimized(&patches); + + patches +} + +fn mark_patches_optimized(patches: &Patches) { + mark_optimized(patches.indices(), &SESSION).unwrap(); + mark_optimized(patches.values(), &SESSION).unwrap(); + if let Some(chunk_offsets) = patches.chunk_offsets() { + mark_optimized(chunk_offsets, &SESSION).unwrap(); + } } fn indices(array_len: usize, n_indices: usize, rng: &mut StdRng) -> ArrayRef { - Buffer::from_iter((0..n_indices).map(|_| rng.random_range(0..(array_len as u64)))).into_array() + let indices = + Buffer::from_iter((0..n_indices).map(|_| rng.random_range(0..(array_len as u64)))) + .into_array(); + mark_optimized(&indices, &SESSION).unwrap(); + + indices } diff --git a/vortex-array/benches/take_primitive.rs b/vortex-array/benches/take_primitive.rs index 7f42da46689..c774b77aec6 100644 --- a/vortex-array/benches/take_primitive.rs +++ b/vortex-array/benches/take_primitive.rs @@ -21,6 +21,7 @@ use vortex_array::arrays::DictArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -55,6 +56,8 @@ fn primitive_take_u32(bencher: Bencher) { let values = PrimitiveArray::from_iter(0u32..256).into_array(); let indices = PrimitiveArray::from_iter((0..NUM_INDICES).map(|index| index as u32 & 255)).into_array(); + mark_optimized(&values, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); bencher .counter(ItemsCount::new(NUM_INDICES)) @@ -79,6 +82,7 @@ fn dict_canonicalize_uniform(bencher: Bencher, num_indi let codes = PrimitiveArray::from_iter(rng.sample_iter(range).take(num_indices)); let dict = DictArray::try_new(codes.into_array(), values.into_array()).unwrap(); + mark_optimized(dict.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) @@ -98,6 +102,7 @@ fn dict_canonicalize_zipfian(bencher: Bencher, num_indi ); let dict = DictArray::try_new(codes.into_array(), values.into_array()).unwrap(); + mark_optimized(dict.as_array(), &SESSION).unwrap(); bencher .with_inputs(|| (&dict, SESSION.create_execution_ctx())) @@ -122,6 +127,7 @@ fn dict_canonicalize_gt_u8(bencher: Bencher, num_indices: usize) { }), ); let dict = DictArray::try_new(codes.into_array(), values.into_array()).unwrap(); + mark_optimized(dict.as_array(), &SESSION).unwrap(); bencher .counter(ItemsCount::new(num_indices)) diff --git a/vortex-array/benches/take_struct.rs b/vortex-array/benches/take_struct.rs index d84d2639559..9092cf81327 100644 --- a/vortex-array/benches/take_struct.rs +++ b/vortex-array/benches/take_struct.rs @@ -18,6 +18,7 @@ use vortex_array::arrays::StructArray; use vortex_array::dtype::FieldNames; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -63,6 +64,8 @@ fn take_struct_simple(bencher: Bencher) { .map(|_| rng.random_range(0..ARRAY_SIZE) as u64) .collect(); let indices_array = indices.into_array(); + mark_optimized(&struct_array, &SESSION).unwrap(); + mark_optimized(&indices_array, &SESSION).unwrap(); bencher .with_inputs(|| { @@ -106,6 +109,8 @@ fn take_struct_wide(bencher: Bencher, width: usize) { .map(|_| rng.random_range(0..ARRAY_SIZE) as u64) .collect(); let indices_array = indices.into_array(); + mark_optimized(&struct_array, &SESSION).unwrap(); + mark_optimized(&indices_array, &SESSION).unwrap(); bencher .with_inputs(|| { @@ -146,6 +151,8 @@ fn take_struct_sequential_indices(bencher: Bencher) { // Sequential indices for better cache performance let indices: Buffer = (0..TAKE_SIZE as u64).collect(); let indices_array = indices.into_array(); + mark_optimized(&struct_array, &SESSION).unwrap(); + mark_optimized(&indices_array, &SESSION).unwrap(); bencher .with_inputs(|| { diff --git a/vortex-array/benches/take_varbin.rs b/vortex-array/benches/take_varbin.rs index afd69b670e0..7a636a72e49 100644 --- a/vortex-array/benches/take_varbin.rs +++ b/vortex-array/benches/take_varbin.rs @@ -17,6 +17,7 @@ use vortex_array::arrays::VarBinArray; use vortex_array::dtype::DType; use vortex_array::dtype::Nullability; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -48,6 +49,8 @@ fn take_varbin(bencher: Bencher) { .map(|_| rng.random_range(0..ARRAY_SIZE) as u64) .collect(); let indices = indices.into_array(); + mark_optimized(&array, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); bencher .with_inputs(|| (&array, &indices, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/validity_is_valid.rs b/vortex-array/benches/validity_is_valid.rs index eee32f7a695..0d27ec0e0a4 100644 --- a/vortex-array/benches/validity_is_valid.rs +++ b/vortex-array/benches/validity_is_valid.rs @@ -20,6 +20,7 @@ use divan::Bencher; use mimalloc::MiMalloc; use vortex_array::VortexSessionExecute; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_session::VortexSession; @@ -36,9 +37,14 @@ const SIZES: &[usize] = &[256, 1024]; /// Build an array-backed validity (~10% nulls so it is `Validity::Array`). fn array_validity(len: usize) -> Validity { - Validity::from(BitBuffer::from_iter( + let validity = Validity::from(BitBuffer::from_iter( (0..len).map(|i| !i.is_multiple_of(10)), - )) + )); + if let Some(array) = validity.as_array() { + mark_optimized(array, &SESSION).unwrap(); + } + + validity } static SESSION: LazyLock = diff --git a/vortex-array/benches/varbinview_compact.rs b/vortex-array/benches/varbinview_compact.rs index d6ce9bcf01d..3ed2020b2c1 100644 --- a/vortex-array/benches/varbinview_compact.rs +++ b/vortex-array/benches/varbinview_compact.rs @@ -17,6 +17,7 @@ use vortex_array::dtype::DType; use vortex_array::dtype::Nullability; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_buffer::Buffer; use vortex_error::VortexExpect; use vortex_session::VortexSession; @@ -67,6 +68,7 @@ fn compact_impl(bencher: Bencher, (output_size, utilization_pct): (usize, usize) let array = taken .execute::(&mut ctx) .vortex_expect("operation should succeed in benchmark"); + mark_optimized(array.as_array(), &SESSION).vortex_expect("optimize the benchmark input"); bencher .with_inputs(|| (&array, SESSION.create_execution_ctx())) @@ -88,6 +90,7 @@ fn compact_sliced_impl(bencher: Bencher, (output_size, utilization_pct): (usize, let array = sliced .execute::(&mut ctx) .vortex_expect("operation should succeed in benchmark"); + mark_optimized(array.as_array(), &SESSION).vortex_expect("optimize the benchmark input"); bencher .with_inputs(|| (&array, SESSION.create_execution_ctx())) diff --git a/vortex-array/benches/varbinview_fill_null.rs b/vortex-array/benches/varbinview_fill_null.rs index fc874fb947a..1494458e97d 100644 --- a/vortex-array/benches/varbinview_fill_null.rs +++ b/vortex-array/benches/varbinview_fill_null.rs @@ -18,6 +18,7 @@ use vortex_array::dtype::Nullability; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -51,6 +52,7 @@ fn varbinview_fill_null_outlined(bencher: Bencher) { fn bench_fill_null(bencher: Bencher, fill: &str) { let array = fixture(LEN); + mark_optimized(&array, &SESSION).unwrap(); let fill = Scalar::utf8(fill, Nullability::NonNullable); bencher diff --git a/vortex-array/benches/varbinview_zip.rs b/vortex-array/benches/varbinview_zip.rs index 1a39b83ddfc..d209ca7afb7 100644 --- a/vortex-array/benches/varbinview_zip.rs +++ b/vortex-array/benches/varbinview_zip.rs @@ -16,6 +16,7 @@ use vortex_array::dtype::DType; use vortex_array::dtype::Nullability; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_mask::Mask; use vortex_session::VortexSession; @@ -40,14 +41,17 @@ fn varbinview_zip_fragmented_mask(bencher: Bencher) { let len = 2_048; let if_true = fixture(len).into_array(); let if_false = other_fixture(len).into_array(); - let mask = alternating_mask(len); + let mask = alternating_mask(len).into_array(); + mark_optimized(&if_true, &SESSION).unwrap(); + mark_optimized(&if_false, &SESSION).unwrap(); + mark_optimized(&mask, &SESSION).unwrap(); bencher .with_inputs(|| { ( if_true.clone(), if_false.clone(), - mask.clone().into_array(), + mask.clone(), SESSION.create_execution_ctx(), ) }) @@ -66,14 +70,17 @@ fn varbinview_zip_block_mask(bencher: Bencher) { let len = 2_048; let if_true = fixture(len).into_array(); let if_false = other_fixture(len).into_array(); - let mask = block_mask(len, 128); + let mask = block_mask(len, 128).into_array(); + mark_optimized(&if_true, &SESSION).unwrap(); + mark_optimized(&if_false, &SESSION).unwrap(); + mark_optimized(&mask, &SESSION).unwrap(); bencher .with_inputs(|| { ( if_true.clone(), if_false.clone(), - mask.clone().into_array(), + mask.clone(), SESSION.create_execution_ctx(), ) }) diff --git a/vortex-array/benches/widen.rs b/vortex-array/benches/widen.rs index 2bb0fee90c3..9f78543dc9c 100644 --- a/vortex-array/benches/widen.rs +++ b/vortex-array/benches/widen.rs @@ -30,6 +30,7 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::WidenArray; use vortex_array::builtins::ArrayBuiltins; use vortex_array::dtype::DType; +use vortex_array::dtype::NativePType; use vortex_array::dtype::Nullability; use vortex_array::dtype::PType; use vortex_array::memory::MemorySession; @@ -37,6 +38,7 @@ use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::fns::between::BetweenOptions; use vortex_array::scalar_fn::fns::between::StrictComparison; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_buffer::Buffer; @@ -134,14 +136,20 @@ impl Values { let validity = self.validity(); let dtype = DType::Primitive(PType::I64, validity.nullability()); let values = PrimitiveArray::new(self.narrow.clone(), validity).into_array(); - WidenArray::try_new(values, dtype) + let array = WidenArray::try_new(values, dtype) .vortex_expect("i16 values with the nullability of the dtype can widen to i64") - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).vortex_expect("optimize the benchmark input"); + + array } /// Builds a new canonical `i64` array of the values. fn canonical(&self) -> ArrayRef { - PrimitiveArray::new(self.wide.clone(), self.validity()).into_array() + let array = PrimitiveArray::new(self.wide.clone(), self.validity()).into_array(); + mark_optimized(&array, &SESSION).vortex_expect("optimize the benchmark input"); + + array } } @@ -152,7 +160,17 @@ fn random_bits(seed: u64, density: f64) -> BitBuffer { } fn constant(value: i64) -> ArrayRef { - ConstantArray::new(value, LEN).into_array() + let array = ConstantArray::new(value, LEN).into_array(); + mark_optimized(&array, &SESSION).vortex_expect("optimize the benchmark input"); + + array +} + +fn index_array(indices: &Buffer) -> ArrayRef { + let array = indices.clone().into_array(); + mark_optimized(&array, &SESSION).vortex_expect("optimize the benchmark input"); + + array } /// Runs `operation` on operands that `setup` builds anew for each iteration. @@ -213,7 +231,7 @@ fn take_few_indices(bencher: Bencher) { let indices: Buffer = (0..LEN / 4).map(|_| rng.random_range(0..len)).collect(); bench_case( bencher, - || (values.widen(), indices.clone().into_array()), + || (values.widen(), index_array(&indices)), |(array, indices), ctx| execute_canonical(array.take(indices), ctx), ); } @@ -232,7 +250,7 @@ fn take_many_indices(bencher: Bencher) { .collect(); bench_case( bencher, - || (values.widen(), indices.clone().into_array()), + || (values.widen(), index_array(&indices)), |(array, indices), ctx| execute_canonical(array.take(indices), ctx), ); } @@ -348,6 +366,8 @@ fn zip(bencher: Bencher) { bencher, || { let mask = selection.clone().into_array(); + mark_optimized(&mask, &SESSION).vortex_expect("optimize the benchmark input"); + (mask, if_true.widen(), if_false.widen()) }, |(mask, if_true, if_false), ctx| execute_canonical(mask.zip(if_true, if_false), ctx), @@ -382,8 +402,8 @@ fn interleave(bencher: Bencher) { || { ( vec![first.widen(), second.widen()], - array_indices.clone().into_array(), - row_indices.clone().into_array(), + index_array(&array_indices), + index_array(&row_indices), ) }, |(values, array_indices, row_indices), ctx| { diff --git a/vortex-array/benches/widen_decimal.rs b/vortex-array/benches/widen_decimal.rs index 2f7584a9b5c..8ccb87094da 100644 --- a/vortex-array/benches/widen_decimal.rs +++ b/vortex-array/benches/widen_decimal.rs @@ -34,6 +34,7 @@ use vortex_array::dtype::DecimalType; use vortex_array::dtype::Nullability; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_buffer::Buffer; @@ -202,8 +203,9 @@ impl Values { } } + /// Builds a new array of the values for `case`, marked as optimized. fn array(&self, case: Case) -> ArrayRef { - match case.input { + let array = match case.input { Input::Canonical => self.decimal_array(case.decimal_dtype()).into_array(), Input::Widen => { let child_dtype = DecimalDType::new(case.child_precision, SCALE); @@ -213,7 +215,10 @@ impl Values { .vortex_expect("encode_decimal selected a strictly narrower child dtype") .into_array() } - } + }; + mark_optimized(&array, &SESSION).vortex_expect("optimize the benchmark input"); + + array } } diff --git a/vortex-array/src/array/erased.rs b/vortex-array/src/array/erased.rs index a96c67e8a3a..22d7a1598e5 100644 --- a/vortex-array/src/array/erased.rs +++ b/vortex-array/src/array/erased.rs @@ -111,6 +111,11 @@ impl ArrayRef { &self.0.optimized } + /// Returns whether another owner holds this array too. + pub(crate) fn is_shared(&self) -> bool { + Arc::strong_count(&self.0) > 1 + } + /// Returns the Arc::as_ptr().addr() of the underlying array. /// This function is used in a couple of places, and we should migrate them to using array_eq. #[doc(hidden)] diff --git a/vortex-array/src/arrays/chunked/vtable/mod.rs b/vortex-array/src/arrays/chunked/vtable/mod.rs index cfd8b94cb90..af6709b3f2b 100644 --- a/vortex-array/src/arrays/chunked/vtable/mod.rs +++ b/vortex-array/src/arrays/chunked/vtable/mod.rs @@ -279,6 +279,8 @@ impl VTable for Chunked { fn reduce(array: ArrayView<'_, Self>) -> VortexResult> { Ok(match array.nchunks() { + // Variant has no empty canonical form; canonicalization handles empty variant chunks. + 0 if matches!(array.dtype(), DType::Variant(_)) => None, 0 => Some(Canonical::empty(array.dtype()).into_array()), 1 => Some(array.chunk(0).clone()), _ => None, diff --git a/vortex-array/src/executor.rs b/vortex-array/src/executor.rs index b261c3175be..86ec920b4c3 100644 --- a/vortex-array/src/executor.rs +++ b/vortex-array/src/executor.rs @@ -3,8 +3,8 @@ //! Iterative array execution. //! -//! The single-step [`Executable`] implementation for [`ArrayRef`] tries `reduce`, -//! `reduce_parent`, `execute_parent`, then `execute` once. The matcher-driven +//! The single-step [`Executable`] implementation for [`ArrayRef`] optimizes the array with the +//! context's session, then tries `execute_parent`, then `execute` once. The matcher-driven //! [`ArrayRef::execute_until`] loop interprets [`ExecutionStep::ExecuteSlot`], //! [`ExecutionStep::AppendChild`], and [`ExecutionStep::Done`] using an explicit stack plus an //! optional builder, so encodings can advance without recursive descent. @@ -44,6 +44,8 @@ use crate::matcher::Matcher; use crate::memory::BufferAllocatorRef; use crate::memory::MemorySessionExt; use crate::optimizer::ArrayOptimizer; +use crate::optimizer::Optimizer; +use crate::optimizer::kernels::ArrayKernels; use crate::optimizer::kernels::ArrayKernelsExt; use crate::optimizer::kernels::ParentExecutionKernels; use crate::optimizer::kernels::execute_parent_key; @@ -179,7 +181,17 @@ impl ArrayRef { self, ctx: &mut ExecutionCtx, ) -> VortexResult { - let mut current_array = self; + // One optimizer serves the whole execution, so a shared node that one step rewrote is + // reused when a later step reaches it. The session is cloned so the optimizer does not + // borrow `ctx`. + let session = ctx.session().clone(); + let session_kernels = session.kernels(); + let session_kernels: &ArrayKernels = &session_kernels; + let mut optimizer = Optimizer::new(Some(session_kernels)); + + // Each node is optimized when execution reaches it: the root here, with the nodes below it + // that were never optimized, and a child when execution focuses it. + let mut current_array = optimizer.optimize_array(&self)?; let mut current_builder: Option> = None; let mut stack: Vec = Vec::new(); let execute_parent_kernels = Arc::clone(&ctx.execute_parent_kernels); @@ -214,6 +226,10 @@ impl ArrayRef { current_builder.is_none(), "root activation should not retain a builder" ); + // Kernels build their results with lazy constructors too, such as a + // filtered validity, so optimize whatever execution produced. This is + // free when the array is already marked as optimized. + current_array = optimizer.optimize_array(¤t_array)?; trace_op!(record_execute_until_return(¤t_array)); return Ok(current_array); } @@ -250,7 +266,7 @@ impl ArrayRef { } { let frame = stack.pop().vortex_expect("just peeked"); - let optimized = result.optimize_ctx(ctx.session())?; + let optimized = optimizer.optimize_array(&result)?; trace_op!(record_execute_optimized(&result, &optimized)); current_array = optimized; current_builder = frame.parent_builder; @@ -268,7 +284,7 @@ impl ArrayRef { && has_execute_parent_kernels(kernels, ¤t_array) && let Some(rewritten) = try_execute_parent(¤t_array, kernels, ctx)? { - let optimized = rewritten.optimize_ctx(ctx.session())?; + let optimized = optimizer.optimize_array(&rewritten)?; trace_op!(record_execute_optimized(&rewritten, &optimized)); current_array = optimized; continue; @@ -302,7 +318,7 @@ impl ArrayRef { original_dtype: child.dtype().clone(), original_len: child.len(), }); - current_array = child; + current_array = optimizer.optimize_array(&child)?; current_builder = None; } ExecutionStep::AppendChild(i) => { @@ -343,6 +359,9 @@ impl ArrayRef { if had_builder { trace_op!(record_builder_finish(¤t_array)); } + // Kernels build their results with lazy constructors, such as a cast that only + // changes nullability. Reduce them before execute-parent kernels see them. + current_array = optimizer.optimize_array(¤t_array)?; } } } @@ -476,7 +495,7 @@ impl Drop for ExecutionCtx { /// Single-step execution: takes one step toward canonical form. /// -/// Steps through reduce, reduce_parent, execute_parent, then execute. For `ExecuteSlot`, +/// Steps through optimize, execute_parent, then execute. For `ExecuteSlot`, /// only a single child execution step is performed — the child is executed once and put back, /// making this a lightweight, bounded operation. /// @@ -496,34 +515,13 @@ impl Executable for ArrayRef { } trace_op!(record_single_step_phase_none("canonical", &array)); - if let Some(reduced) = array.reduce()? { - reduced.statistics().inherit_from(array.statistics()); - trace_op!(record_single_step_applied("reduce", &array, &reduced)); - return Ok(reduced); + let optimized = array.optimize_ctx(ctx.session())?; + if !ArrayRef::ptr_eq(&optimized, &array) { + optimized.statistics().inherit_from(array.statistics()); + trace_op!(record_single_step_applied("optimize", &array, &optimized)); + return Ok(optimized); } - trace_op!(record_single_step_phase_none("reduce", &array)); - - for (slot_idx, slot) in array.slots().iter().enumerate() { - let Some(child) = slot else { continue }; - if let Some(reduced_parent) = child.reduce_parent(&array, slot_idx)? { - log_parent_rewrite( - ctx, - "reduce_parent", - slot_idx, - &array, - child, - &reduced_parent, - ); - reduced_parent.statistics().inherit_from(array.statistics()); - trace_op!(record_single_step_applied( - "reduce_parent", - &array, - &reduced_parent, - )); - return Ok(reduced_parent); - } - } - trace_op!(record_single_step_phase_none("reduce_parent", &array)); + trace_op!(record_single_step_phase_none("optimize", &array)); let execute_parent_kernels = Arc::clone(&ctx.execute_parent_kernels); let kernels = execute_parent_kernels.as_ref(); diff --git a/vortex-array/src/optimizer/mod.rs b/vortex-array/src/optimizer/mod.rs index 5384988671f..b5b144f474f 100644 --- a/vortex-array/src/optimizer/mod.rs +++ b/vortex-array/src/optimizer/mod.rs @@ -9,19 +9,34 @@ //! //! There are three public entry points on [`ArrayOptimizer`]: //! -//! - [`ArrayOptimizer::optimize`] uses only static rules registered on encoding vtables. +//! - [`ArrayOptimizer::optimize`] uses only static rules registered on encoding vtables, and only +//! rewrites the root node. //! - [`ArrayOptimizer::optimize_ctx`] also consults the session's active //! [`kernels::ArrayKernels`] registry before static parent-reduce rules, so this is the entry -//! point used by execution. -//! - [`ArrayOptimizer::optimize_recursive`] applies the session-aware optimizer to the root and -//! every descendant. +//! point used by execution. It optimizes the root, then the descendants below it that were +//! never optimized, then the root again if a child changed. +//! - [`ArrayOptimizer::optimize_recursive`] is the same as [`ArrayOptimizer::optimize_ctx`]. //! -//! Each array records whether [`ArrayOptimizer::optimize_recursive`] has optimized it and all of -//! its descendants. A new array starts unoptimized, and the optimizer marks the arrays that -//! [`ArrayOptimizer::optimize_recursive`] returns, so optimizing a marked array again returns it -//! without trying any rule. The mark does not record the session: an array optimized with one -//! session is not optimized again with a session that registers more kernels. - +//! With a session, the optimizer works top down. It first tries the rules of a node on its children +//! as they are, so a rule that drops or replaces a child does not optimize that child first. Then +//! it optimizes the children that were never optimized, and if any child changed, it tries the +//! rules of the node again. Thus a parent rule or kernel keyed on a child's encoding still sees +//! that encoding rather than a lazy wrapper around it, such as a slice of an FSST array under a +//! `LIKE`. Children already optimized are left without visiting their subtrees. +//! +//! Whenever a rule rewrites a node, the nodes the rule built are optimized bottom up before the +//! next pass on the node. Otherwise two rules that are inverses of each other, such as +//! `slice(get_item(s))` and `get_item(slice(s))`, could rewrite the node back and forth. +//! +//! Each array records whether the optimizer has taken it to its fixpoint. A new array starts +//! unoptimized, and optimizing with a session marks the arrays it returns, so optimizing an array +//! again returns it without trying any rule. The optimizer marks a node only after it optimized +//! the children of the node, so the descendants of a marked node are marked too. The mark does not +//! record the session: an array optimized with one session is not optimized again with a session +//! that registers more kernels. + +use std::hash::Hash; +use std::hash::Hasher; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -29,6 +44,7 @@ use smallvec::SmallVec; use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_session::VortexSession; +use vortex_utils::aliases::hash_map::HashMap; use crate::ArrayRef; use crate::optimizer::kernels::ArrayKernels; @@ -113,19 +129,20 @@ pub trait ArrayOptimizer { /// when a session-scoped [`kernels::ArrayKernels`] registry should participate. fn optimize(&self) -> VortexResult; - /// Optimize the root array node using static rules and the active - /// [`kernels::ArrayKernels`] registry on `session`, if any. + /// Optimize the array tree using static rules and the active [`kernels::ArrayKernels`] + /// registry on `session`, if any. /// - /// Session kernels are checked for each `(parent_encoding_id, child_encoding_id)` pair before - /// the child's static `PARENT_RULES`. The registry comes from the [`kernels::KernelSession`] on - /// `session`, if any. If `session` does not contain a [`kernels::KernelSession`], this behaves - /// like [`Self::optimize`]. + /// The descendants that were never optimized are optimized before the root. Session kernels + /// are checked for each `(parent_encoding_id, child_encoding_id)` pair before the child's + /// static `PARENT_RULES`. The registry comes from the [`kernels::KernelSession`] on `session`, + /// if any. If `session` does not contain a [`kernels::KernelSession`], this behaves like + /// [`Self::optimize`] on the root and its never optimized descendants. fn optimize_ctx(&self, session: &VortexSession) -> VortexResult; /// Optimize the entire array tree recursively (root and all descendants). /// - /// This uses the same session-aware rule ordering as [`Self::optimize_ctx`] for every node in - /// the tree. + /// This is the same as [`Self::optimize_ctx`]: optimizing a node first optimizes every + /// descendant that was never optimized. fn optimize_recursive(&self, session: &VortexSession) -> VortexResult; } @@ -135,7 +152,7 @@ impl ArrayOptimizer for ArrayRef { return Ok(self.clone()); } - Ok(optimize_owned(self.clone(), None)?.0) + Optimizer::new(None).optimize_root(self.clone()) } fn optimize_ctx(&self, session: &VortexSession) -> VortexResult { @@ -143,73 +160,237 @@ impl ArrayOptimizer for ArrayRef { return Ok(self.clone()); } - Ok(optimize_owned(self.clone(), Some(session))?.0) + let session_kernels = session.kernels(); + let session_kernels: &ArrayKernels = &session_kernels; + Optimizer::new(Some(session_kernels)).optimize_array(self) } fn optimize_recursive(&self, session: &VortexSession) -> VortexResult { - Ok(try_optimize_recursive(self.clone(), session)?.0) + self.optimize_ctx(session) } } -fn optimize_owned( - mut current_array: ArrayRef, - session: Option<&VortexSession>, -) -> VortexResult<(ArrayRef, bool)> { - let mut any_optimizations = false; - let session_kernels = session.map(|session| session.kernels()); +/// An array keyed by identity: two keys are equal only when they hold the same array, as +/// [`ArrayRef::ptr_eq`] decides. +/// +/// The key holds the array, so its address cannot be freed and reused by another array while the +/// key exists. +struct ArrayByPtr(ArrayRef); - trace_op!(record_optimize_start(¤t_array, session.is_some())); +impl PartialEq for ArrayByPtr { + fn eq(&self, other: &Self) -> bool { + ArrayRef::ptr_eq(&self.0, &other.0) + } +} - for _ in 0..=MAX_OPTIMIZER_REWRITE_PASS { - trace_op!(record_optimize_loop_start(¤t_array)); +impl Eq for ArrayByPtr {} - if let Some(new_array) = current_array.reduce()? { - current_array = new_array; - any_optimizations = true; - trace_op!(record_optimize_loop_end()); - continue; +impl Hash for ArrayByPtr { + fn hash(&self, state: &mut H) { + self.0.addr().hash(state); + } +} + +/// The optimizer state, which can be kept for many optimize calls. +/// +/// Execution keeps one optimizer for all of [`ArrayRef::execute_until`], so a shared node that one +/// step rewrote is not rewritten again when a later step reaches it through another parent. +pub(crate) struct Optimizer<'a> { + /// The session kernels, tried before the static parent-reduce rules. + /// + /// Without them, the optimizer rewrites only the root node and does not mark the result, so + /// optimizing it later with a session still tries the session kernels. + session_kernels: Option<&'a ArrayKernels>, + /// The shared nodes this optimizer rewrote, each with the array it was rewritten into. + /// + /// Many parents can hold the same unoptimized node, such as the struct below every field of a + /// projection. A rewrite returns a new array and does not mark the node, so without this each + /// parent would rewrite the node and its whole subtree again. The key keeps the node alive + /// for the life of the optimizer. + rewritten_shared: HashMap, +} + +impl<'a> Optimizer<'a> { + /// Creates an optimizer that tries `session_kernels` before the static parent-reduce rules. + pub(crate) fn new(session_kernels: Option<&'a ArrayKernels>) -> Self { + Self { + session_kernels, + rewritten_shared: HashMap::new(), } + } - trace_op!(record_optimize_reduce_none(¤t_array)); + /// Optimizes `array` and the descendants below it that were never optimized, and marks the + /// result. + /// + /// If another owner holds `array` too, the rewrite is remembered, and a later call with the + /// same array returns it. + pub(crate) fn optimize_array(&mut self, array: &ArrayRef) -> VortexResult { + if !array.is_shared() { + return self.optimize(array.clone()); + } - // Try children in order; the first parent rewrite restarts the fixpoint loop. - let mut reduced_parent = None; - for (slot_idx, slot) in current_array.slots().iter().enumerate() { - let Some(child) = slot else { - continue; - }; + let key = ArrayByPtr(array.clone()); + if let Some(rewritten) = self.rewritten_shared.get(&key) { + return Ok(rewritten.clone()); + } - // Session kernels take precedence over the child's static parent-reduce rules. - if let Some(session_kernels) = &session_kernels - && let Some(new_array) = - try_session_parent_reduce(session_kernels, ¤t_array, child, slot_idx)? - { - reduced_parent = Some(new_array); - break; - } + let optimized = self.optimize(array.clone())?; + if !ArrayRef::ptr_eq(&optimized, array) { + self.rewritten_shared.insert(key, optimized.clone()); + } - if let Some(new_array) = child.reduce_parent(¤t_array, slot_idx)? { - reduced_parent = Some(new_array); + Ok(optimized) + } + + /// Optimizes the root of `array` top down: first the root, then the descendants below it that + /// were never optimized, then the root again while its children change. Marks the result. + fn optimize(&mut self, array: ArrayRef) -> VortexResult { + if array.optimized_state().is_optimized() { + return Ok(array); + } + + // The rules of the root get the first try, on children that can still be lazy, so a rule + // that drops or replaces a child does not pay to optimize that child's subtree first. + let mut optimized = self.optimize_root(array.clone())?; + + for _ in 0..=MAX_OPTIMIZER_REWRITE_PASS { + let with_children = self.optimize_new_nodes(optimized.clone())?; + if ArrayRef::ptr_eq(&with_children, &optimized) { break; } + + // A child changed its encoding, so a parent rule keyed on that encoding can apply now. + optimized = self.optimize_root(with_children)?; } - if let Some(new_array) = reduced_parent { - current_array = new_array; - any_optimizations = true; + // A rewrite keeps the logical values, so every statistic of the input stays valid. Lazy + // constructors such as `slice` put the statistics they carry over on the lazy node. + if !ArrayRef::ptr_eq(&optimized, &array) { + optimized.statistics().inherit_from(array.statistics()); + } + + optimized.optimized_state().mark_optimized(); + Ok(optimized) + } + + /// Runs the reduce and parent-reduce rules on the root of `current_array` to fixpoint. + /// + /// With session kernels, the nodes each rewrite built are optimized before the next pass. + fn optimize_root(&mut self, mut current_array: ArrayRef) -> VortexResult { + let mut any_optimizations = false; + + trace_op!(record_optimize_start( + ¤t_array, + self.session_kernels.is_some() + )); + + for _ in 0..=MAX_OPTIMIZER_REWRITE_PASS { + trace_op!(record_optimize_loop_start(¤t_array)); + + if let Some(new_array) = current_array.reduce()? { + current_array = self.optimize_rewritten(new_array)?; + any_optimizations = true; + trace_op!(record_optimize_loop_end()); + continue; + } + + trace_op!(record_optimize_reduce_none(¤t_array)); + + // Try children in order; the first parent rewrite restarts the fixpoint loop. + let mut reduced_parent = None; + for (slot_idx, slot) in current_array.slots().iter().enumerate() { + let Some(child) = slot else { + continue; + }; + + // Session kernels take precedence over the child's static parent-reduce rules. + if let Some(session_kernels) = self.session_kernels + && let Some(new_array) = + try_session_parent_reduce(session_kernels, ¤t_array, child, slot_idx)? + { + reduced_parent = Some(new_array); + break; + } + + if let Some(new_array) = child.reduce_parent(¤t_array, slot_idx)? { + reduced_parent = Some(new_array); + break; + } + } + + if let Some(new_array) = reduced_parent { + current_array = self.optimize_rewritten(new_array)?; + any_optimizations = true; + trace_op!(record_optimize_loop_end()); + continue; + } + + trace_op!(record_optimize_parent_reduce_none(¤t_array)); trace_op!(record_optimize_loop_end()); - continue; + + trace_op!(record_optimize_done(¤t_array, any_optimizations)); + + return Ok(current_array); } - trace_op!(record_optimize_parent_reduce_none(¤t_array)); - trace_op!(record_optimize_loop_end()); + vortex_bail!("Exceeded maximum optimization iterations (possible infinite loop)"); + } - trace_op!(record_optimize_done(¤t_array, any_optimizations)); + /// Optimizes the nodes that a rule built below the array it returned, if the optimizer has + /// session kernels. + fn optimize_rewritten(&mut self, array: ArrayRef) -> VortexResult { + if self.session_kernels.is_none() { + return Ok(array); + } - return Ok((current_array, any_optimizations)); + self.optimize_new_nodes(array) } - vortex_bail!("Exceeded maximum optimization iterations (possible infinite loop)"); + /// Optimizes the nodes below `array` that were never optimized, such as those a rule built + /// below its result. + /// + /// Rules and constructors build lazy arrays, so these nodes are not yet optimized, at any + /// depth. Optimizing them as each rewrite happens keeps the optimizer from undoing one rule + /// with its inverse: `slice(get_item(s))` and `get_item(slice(s))` rewrite into each other + /// until the inner `slice(s)` collapses into `s`'s own encoding. + /// + /// A child that is already optimized is left as it is without visiting its subtree, which + /// skips the subtrees a rule carries over from the array it rewrote. + fn optimize_new_nodes(&mut self, array: ArrayRef) -> VortexResult { + let mut new_slots: Option> = None; + for (slot_idx, slot) in array.slots().iter().enumerate() { + let optimized = match slot { + Some(child) if !child.optimized_state().is_optimized() => { + let optimized = self.optimize_array(child)?; + (!ArrayRef::ptr_eq(&optimized, child)).then_some(optimized) + } + _ => None, + }; + + match optimized { + Some(optimized) => new_slots + .get_or_insert_with(|| { + let mut slots = SmallVec::with_capacity(array.slots().len()); + slots.extend(array.slots()[..slot_idx].iter().cloned()); + slots + }) + .push(Some(optimized)), + None => { + if let Some(new_slots) = &mut new_slots { + new_slots.push(slot.clone()); + } + } + } + } + + let Some(new_slots) = new_slots else { + return Ok(array); + }; + + // SAFETY: optimizer rules only replace child slots with logically equivalent arrays, so + // the parent's logical values and statistics remain valid. + unsafe { array.with_slots(new_slots) } + } } fn try_session_parent_reduce( @@ -249,51 +430,6 @@ fn try_session_parent_reduce( Ok(None) } -fn try_optimize_recursive( - current_array: ArrayRef, - session: &VortexSession, -) -> VortexResult<(ArrayRef, bool)> { - if current_array.optimized_state().is_optimized() { - return Ok((current_array, false)); - } - - trace_op!(record_optimize_recursive_start(¤t_array)); - - let (mut current_array, mut any_optimizations) = optimize_owned(current_array, Some(session))?; - - let mut new_slots = SmallVec::with_capacity(current_array.slots().len()); - let mut any_slot_optimized = false; - for slot in current_array.slots() { - match slot { - Some(child) => { - let (new_child, new_slot_optimized) = - try_optimize_recursive(child.clone(), session)?; - if new_slot_optimized { - trace_op!(record_optimize_recursive_slot( - new_slots.len(), - child, - &new_child, - )); - } - new_slots.push(Some(new_child)); - any_slot_optimized |= new_slot_optimized; - } - None => new_slots.push(None), - } - } - - if any_slot_optimized { - // SAFETY: optimizer rules only replace child slots with logically equivalent arrays, so - // parent logical values and statistics remain valid. - current_array = unsafe { current_array.with_slots(new_slots) }?; - any_optimizations = true; - } - - // Every child is marked now, so the mark on this node covers its whole subtree. - current_array.optimized_state().mark_optimized(); - Ok((current_array, any_optimizations)) -} - #[cfg(test)] mod tests { use std::sync::LazyLock; @@ -308,45 +444,104 @@ mod tests { use crate::arrays::FilterArray; use crate::arrays::Primitive; use crate::arrays::SliceArray; + use crate::arrays::StructArray; + use crate::expr::stats::Precision; + use crate::expr::stats::Stat; + use crate::expr::stats::StatsProviderExt; use crate::optimizer::ArrayOptimizer; + use crate::scalar::ScalarValue; static SESSION: LazyLock = LazyLock::new(VortexSession::empty); + fn lazy_slice(array: ArrayRef, range: std::ops::Range) -> VortexResult { + Ok(SliceArray::try_new(array, range)?.into_array()) + } + #[test] - fn optimize_recursive_marks_result_and_skips_it_after() -> VortexResult<()> { - let sliced = SliceArray::try_new(buffer![1i32, 2, 3, 4].into_array(), 1..3)?.into_array(); + fn optimize_marks_result_and_skips_it_after() -> VortexResult<()> { + let sliced = lazy_slice(buffer![1i32, 2, 3, 4].into_array(), 1..3)?; assert!(!sliced.optimized_state().is_optimized()); - let optimized = sliced.optimize_recursive(&SESSION)?; + let optimized = sliced.optimize_ctx(&SESSION)?; assert!(optimized.is::()); assert!(optimized.optimized_state().is_optimized()); - assert!(ArrayRef::ptr_eq( - &optimized.optimize_recursive(&SESSION)?, - &optimized - )); assert!(ArrayRef::ptr_eq( &optimized.optimize_ctx(&SESSION)?, &optimized )); // A new array built over an optimized one starts unoptimized. - let resliced = SliceArray::try_new(optimized, 0..1)?.into_array(); - assert!(!resliced.optimized_state().is_optimized()); + assert!( + !lazy_slice(optimized, 0..1)? + .optimized_state() + .is_optimized() + ); + Ok(()) + } + + #[test] + fn optimize_without_session_does_not_mark() -> VortexResult<()> { + let sliced = lazy_slice(buffer![1i32, 2, 3, 4].into_array(), 1..3)?; + + let optimized = sliced.optimize()?; + assert!(optimized.is::()); + assert!(!optimized.optimized_state().is_optimized()); + Ok(()) + } + + #[test] + fn optimize_rewrites_a_shared_node_once() -> VortexResult<()> { + // Both fields hold the same unoptimized slice. The second field reuses the rewrite of the + // first, so the optimizer does not rewrite the slice and its subtree again. + let shared = lazy_slice(buffer![1i32, 2, 3, 4].into_array(), 1..3)?; + let parent = + StructArray::from_fields(&[("a", shared.clone()), ("b", shared)])?.into_array(); + + let optimized = parent.optimize_ctx(&SESSION)?; + let fields = optimized.slots().iter().flatten().collect::>(); + assert_eq!(fields.len(), 2); + assert!(fields[0].is::()); + assert!(ArrayRef::ptr_eq(fields[0], fields[1])); Ok(()) } #[test] - fn optimize_recursive_marks_every_descendant() -> VortexResult<()> { - let sliced = SliceArray::try_new(buffer![1i32, 2, 3, 4].into_array(), 1..4)?.into_array(); + fn optimize_marks_every_descendant() -> VortexResult<()> { + let sliced = lazy_slice(buffer![1i32, 2, 3, 4].into_array(), 1..4)?; let filtered = FilterArray::try_new(sliced, Mask::from_iter([true, false, true]))?.into_array(); - let optimized = filtered.optimize_recursive(&SESSION)?; + let optimized = filtered.optimize_ctx(&SESSION)?; assert!( optimized .depth_first_traversal() .all(|node| node.optimized_state().is_optimized()) ); + assert!(ArrayRef::ptr_eq( + &optimized.optimize_recursive(&SESSION)?, + &optimized + )); + Ok(()) + } + + #[test] + fn optimize_keeps_stats_of_rewritten_array() -> VortexResult<()> { + let array = buffer![1i32, 2, 3, 4].into_array(); + array + .statistics() + .set(Stat::IsSorted, Precision::exact(ScalarValue::Bool(true))); + + let sliced = lazy_slice(array, 1..3)?; + sliced + .statistics() + .set(Stat::IsSorted, Precision::exact(ScalarValue::Bool(true))); + + let optimized = sliced.optimize_ctx(&SESSION)?; + assert!(optimized.is::()); + assert_eq!( + optimized.statistics().get_as::(Stat::IsSorted), + Precision::exact(true) + ); Ok(()) } } diff --git a/vortex-array/src/test_harness/mod.rs b/vortex-array/src/test_harness/mod.rs index 497a854c58d..19197f8b59b 100644 --- a/vortex-array/src/test_harness/mod.rs +++ b/vortex-array/src/test_harness/mod.rs @@ -11,10 +11,14 @@ use goldenfile::Mint; use goldenfile::differs::binary_diff; use itertools::Itertools; use vortex_error::VortexResult; +use vortex_session::VortexSession; +use crate::ArrayRef; use crate::ExecutionCtx; use crate::arrays::BoolArray; use crate::arrays::bool::BoolArrayExt; +use crate::optimizer::ArrayOptimizer; +use crate::optimizer::mark_decoded_optimized; mod execution_paths; pub use execution_paths::ExpectedOutcome; @@ -48,3 +52,36 @@ pub fn to_int_indices(indices_bits: BoolArray, ctx: &mut ExecutionCtx) -> Vortex .filter_map(|(idx, v)| (v && mask.value(idx)).then_some(idx as u64)) .collect_vec()) } + +/// Marks a benchmark input as optimized, as decoding marks the arrays that a reader returns. +/// +/// Execution optimizes each array that it reaches unless the array is already marked. An input +/// that a benchmark builds with constructors is not marked, so without this call the benchmark +/// also times the optimizer. +/// +/// # Panics +/// +/// Panics if the optimizer rewrites `array`. Then the benchmark would time an array other than +/// the one it built. +pub fn mark_optimized(array: &ArrayRef, session: &VortexSession) -> VortexResult<()> { + let optimized = array.optimize_ctx(session)?; + assert!( + ArrayRef::ptr_eq(&optimized, array), + "the optimizer rewrote the benchmark input:\n{}\ninto:\n{}", + array.display_tree(), + optimized.display_tree(), + ); + + Ok(()) +} + +/// Marks each node of a benchmark input as optimized without trying any rule, as decoding does. +/// +/// Use this instead of [`mark_optimized`] when the benchmark times a shape that a file can hold +/// but that the optimizer rewrites, such as an extension array over constant storage. Decoding +/// keeps that shape, so the benchmark must keep it too. +pub fn mark_decoded(array: &ArrayRef) { + for node in array.depth_first_traversal() { + mark_decoded_optimized(&node); + } +} diff --git a/vortex-array/src/test_harness/trace/mod.rs b/vortex-array/src/test_harness/trace/mod.rs index 1e053cf5a7a..e3a1eca0777 100644 --- a/vortex-array/src/test_harness/trace/mod.rs +++ b/vortex-array/src/test_harness/trace/mod.rs @@ -409,20 +409,6 @@ pub(crate) fn record_optimize_done(output: &ArrayRef, changed: bool) { }); } -pub(crate) fn record_optimize_recursive_start(root: &ArrayRef) { - record(TraceEvent::OptimizeRecursiveStart { - root: ArraySummary::new(root), - }); -} - -pub(crate) fn record_optimize_recursive_slot(slot_idx: usize, input: &ArrayRef, output: &ArrayRef) { - record(TraceEvent::OptimizeRecursiveSlot { - slot_idx, - input: ArraySummary::new(input), - output: ArraySummary::new(output), - }); -} - pub(crate) fn record_reduce_applied(array: &ArrayRef, rule: &dyn Debug, output: &ArrayRef) { record(TraceEvent::ReduceApplied { array: ArraySummary::new(array), @@ -887,14 +873,6 @@ enum TraceEvent { output: ArraySummary, changed: bool, }, - OptimizeRecursiveStart { - root: ArraySummary, - }, - OptimizeRecursiveSlot { - slot_idx: usize, - input: ArraySummary, - output: ArraySummary, - }, ReduceAttempt { array: ArraySummary, rule: String, @@ -1065,12 +1043,10 @@ impl TraceEvent { | TraceEvent::SlotTransition { .. } | TraceEvent::BuilderEvent { .. } | TraceEvent::ExecuteDone { .. } => 2, - TraceEvent::OptimizeRecursiveSlot { .. } - | TraceEvent::ExecuteUntilIteration { .. } + TraceEvent::ExecuteUntilIteration { .. } | TraceEvent::ExecuteUntilReturn { .. } | TraceEvent::SingleStepApplied { .. } => 1, TraceEvent::OptimizeLoopEnd - | TraceEvent::OptimizeRecursiveStart { .. } | TraceEvent::ExecuteUntilStart { .. } | TraceEvent::SingleStepStart { .. } => 0, } @@ -1089,14 +1065,6 @@ impl TraceEvent { TraceResolution::Attempts => write!(f, "done output={output} changed={changed}"), TraceResolution::ExecutedOnly => write!(f, "done output={output}"), }, - TraceEvent::OptimizeRecursiveStart { root } => { - write!(f, "optimize_recursive root={root}") - } - TraceEvent::OptimizeRecursiveSlot { - slot_idx, - input, - output, - } => write!(f, "recursive slot={slot_idx} input={input} output={output}"), TraceEvent::ReduceAttempt { array, rule, diff --git a/vortex-array/src/test_harness/trace/tests.rs b/vortex-array/src/test_harness/trace/tests.rs index eaeb3f302f1..ea452860b84 100644 --- a/vortex-array/src/test_harness/trace/tests.rs +++ b/vortex-array/src/test_harness/trace/tests.rs @@ -444,19 +444,20 @@ fn trace_optimize_contiguous_filter() -> VortexResult<()> { &mut execution_ctx() ); insta::assert_snapshot!(traced.trace.to_string(), @" - execute_until target=AnyCanonical root=vortex.filter(i32, len=2) - iter 0 current=vortex.filter(i32, len=2) builder_active=false - optimize root=vortex.slice(i32, len=2) session=false + optimize root=vortex.filter(i32, len=2) session=true optimize root=vortex.slice(i32, len=2) session=false - reduce_parent static:SliceReduceAdaptor(Primitive) slot=0 parent=vortex.slice(i32, len=2) child=vortex.primitive(i32, len=6) -> vortex.primitive(i32, len=2) - done output=vortex.primitive(i32, len=2) - optimize root=vortex.filter(i32, len=2) session=false - reduce TrivialFilterRule: vortex.filter(i32, len=2) -> vortex.primitive(i32, len=2) + optimize root=vortex.slice(i32, len=2) session=false + reduce_parent static:SliceReduceAdaptor(Primitive) slot=0 parent=vortex.slice(i32, len=2) child=vortex.primitive(i32, len=6) -> vortex.primitive(i32, len=2) + done output=vortex.primitive(i32, len=2) + optimize root=vortex.filter(i32, len=2) session=false + reduce TrivialFilterRule: vortex.filter(i32, len=2) -> vortex.primitive(i32, len=2) + done output=vortex.primitive(i32, len=2) + reduce_parent static:SliceReduceAdaptor(Filter) slot=0 parent=vortex.slice(i32, len=2) child=vortex.filter(i32, len=4) -> vortex.primitive(i32, len=2) done output=vortex.primitive(i32, len=2) - reduce_parent static:SliceReduceAdaptor(Filter) slot=0 parent=vortex.slice(i32, len=2) child=vortex.filter(i32, len=4) -> vortex.primitive(i32, len=2) + reduce TrivialFilterRule: vortex.filter(i32, len=2) -> vortex.primitive(i32, len=2) done output=vortex.primitive(i32, len=2) - Done array=vortex.primitive(i32, len=2) - iter 1 current=vortex.primitive(i32, len=2) builder_active=false + execute_until target=AnyCanonical root=vortex.primitive(i32, len=2) + iter 0 current=vortex.primitive(i32, len=2) builder_active=false return output=vortex.primitive(i32, len=2) "); @@ -495,16 +496,13 @@ fn trace_execute_filter_with_scattered_mask() -> VortexResult<()> { &mut execution_ctx() ); insta::assert_snapshot!(traced.trace.to_string(), @" + optimize root=vortex.filter(i32, len=2) session=true + reduce_parent static:FilterReduceAdaptor(Filter) slot=0 parent=vortex.filter(i32, len=2) child=vortex.filter(i32, len=4) -> vortex.filter(i32, len=2) + done output=vortex.filter(i32, len=2) execute_until target=AnyCanonical root=vortex.filter(i32, len=2) iter 0 current=vortex.filter(i32, len=2) builder_active=false - ExecuteSlot slot=0 parent=vortex.filter(i32, len=2) child=vortex.filter(i32, len=4) - iter 1 current=vortex.filter(i32, len=4) stack_parent=vortex.filter(i32, len=2) slot=0 builder_active=false - Done array=vortex.primitive(i32, len=4) - iter 2 current=vortex.primitive(i32, len=4) stack_parent=vortex.filter(i32, len=2) slot=0 builder_active=false - pop_frame slot=0 output=vortex.filter(i32, len=2) - iter 3 current=vortex.filter(i32, len=2) builder_active=false Done array=vortex.primitive(i32, len=2) - iter 4 current=vortex.primitive(i32, len=2) builder_active=false + iter 1 current=vortex.primitive(i32, len=2) builder_active=false return output=vortex.primitive(i32, len=2) "); @@ -532,6 +530,16 @@ fn trace_execution_stack_parent_kernel_attempts( &mut ctx ); insta::assert_snapshot!(traced.trace.to_string(), @" + optimize root=vortex.test.stack-parent(i32, len=3) session=true + loop input=vortex.test.stack-parent(i32, len=3) + reduce none array=vortex.test.stack-parent(i32, len=3) + reduce_parent none array=vortex.test.stack-parent(i32, len=3) + done output=vortex.test.stack-parent(i32, len=3) changed=false + optimize root=vortex.test.stack-child(i32, len=3) session=true + loop input=vortex.test.stack-child(i32, len=3) + reduce none array=vortex.test.stack-child(i32, len=3) + reduce_parent none array=vortex.test.stack-child(i32, len=3) + done output=vortex.test.stack-child(i32, len=3) changed=false execute_until target=AnyCanonical root=vortex.test.stack-parent(i32, len=3) iter 0 current=vortex.test.stack-parent(i32, len=3) builder_active=false done_check target=false canonical=false diff --git a/vortex-array/src/trace_macros.rs b/vortex-array/src/trace_macros.rs index fd66f38b201..31cc6bc3e3f 100644 --- a/vortex-array/src/trace_macros.rs +++ b/vortex-array/src/trace_macros.rs @@ -19,7 +19,9 @@ /// /// - When `test` OR `_test-harness` is on, AND `codspeed` is off, the body is compiled in and /// guarded by a runtime check for an active recorder. -/// - In all other configurations, the body is replaced with `()`. +/// - In all other configurations, the arguments are only borrowed in a branch that never runs. +/// Thus they are not evaluated, and a value that only a trace event reads is not reported +/// as unused. /// /// The macro is named `trace_op` because it records *operations* (optimizer rewrites, parent /// kernels, execution steps, builder activity) rather than array values. The events it emits @@ -32,6 +34,13 @@ macro_rules! trace_op { $crate::test_harness::trace::$event $(::<$($generic),*>)?($($arg),*); } } + + #[cfg(not(all(any(test, feature = "_test-harness"), not(codspeed))))] + { + if false { + $(let _ = &$arg;)* + } + } }}; } diff --git a/vortex-arrow/benches/to_arrow.rs b/vortex-arrow/benches/to_arrow.rs index ae773ce70d7..26790c3789e 100644 --- a/vortex-arrow/benches/to_arrow.rs +++ b/vortex-arrow/benches/to_arrow.rs @@ -37,6 +37,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; use vortex_array::session::ArraySessionExt; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; #[expect( deprecated, @@ -98,7 +99,7 @@ fn schema() -> DType { } fn array() -> ArrayRef { - StructArray::from_fields(&[ + let array = StructArray::from_fields(&[ ( "primitive", PrimitiveArray::from_iter(0i16..1024).into_array(), @@ -118,7 +119,10 @@ fn array() -> ArrayRef { ), ]) .unwrap() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } #[allow(non_snake_case)] @@ -381,7 +385,7 @@ fn chunked_strings( fn string_array(case: StringCase) -> ArrayRef { let mut ctx = SESSION.create_execution_ctx(); - match case.structure { + let array = match case.structure { StringStructure::Flat => encode_strings( structured_strings(STRING_ROWS, case.validity), case.encoding, @@ -389,7 +393,10 @@ fn string_array(case: StringCase) -> ArrayRef { ), StringStructure::Dict => dictionary_strings(case.encoding, case.validity, &mut ctx), StringStructure::Chunked => chunked_strings(case.encoding, case.validity, &mut ctx), - } + }; + mark_optimized(&array, &SESSION).unwrap(); + + array } /// Measures export to Arrow offset arrays and Arrow view arrays. @@ -567,6 +574,8 @@ fn decimal_byte_parts(msp: PrimitiveArray, decimal_dtype: DecimalDType) -> Array #[divan::bench(args = DECIMAL_EXPORT_CASES)] fn decimal_export(bencher: Bencher, case: DecimalExportCase) { let array = case.array(); + mark_optimized(&array, &SESSION).unwrap(); + let decimal_dtype = case.decimal_dtype(); let field = Field::new( "value", diff --git a/vortex-btrblocks/benches/compress.rs b/vortex-btrblocks/benches/compress.rs index 6964538cd0e..813fdb329c0 100644 --- a/vortex-btrblocks/benches/compress.rs +++ b/vortex-btrblocks/benches/compress.rs @@ -26,6 +26,7 @@ mod benchmarks { use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; + use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_btrblocks::BtrBlocksCompressorBuilder; use vortex_btrblocks::CompressionSession; @@ -102,10 +103,10 @@ mod benchmarks { .build(); bencher .with_inputs(|| { - ( - PrimitiveArray::new(values.clone(), Validity::NonNullable).into_array(), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::new(values.clone(), Validity::NonNullable).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .input_counter(|(array, _)| ItemsCount::new(array.len())) .input_counter(|(array, _)| BytesCount::of_many::(array.len())) @@ -126,15 +127,15 @@ mod benchmarks { let compressor = BtrBlocksCompressorBuilder::from_session(&WRITE_SESSION).build(); bencher .with_inputs(|| { - ( - DecimalArray::new( - values.clone(), - DecimalDType::new(12, 2), - Validity::NonNullable, - ) - .into_array(), - WRITE_SESSION.create_execution_ctx(), + let array = DecimalArray::new( + values.clone(), + DecimalDType::new(12, 2), + Validity::NonNullable, ) + .into_array(); + mark_optimized(&array, &WRITE_SESSION).unwrap(); + + (array, WRITE_SESSION.create_execution_ctx()) }) .input_counter(|(array, _)| ItemsCount::new(array.len())) .input_counter(|(array, _)| BytesCount::of_many::(array.len())) diff --git a/vortex-btrblocks/benches/compress_listview.rs b/vortex-btrblocks/benches/compress_listview.rs index e4391ebccfd..118d5e0eec3 100644 --- a/vortex-btrblocks/benches/compress_listview.rs +++ b/vortex-btrblocks/benches/compress_listview.rs @@ -28,6 +28,7 @@ mod benchmarks { use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::session::ArraySession; + use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_btrblocks::BtrBlocksCompressorBuilder; use vortex_btrblocks::CompressionSession; @@ -206,6 +207,8 @@ mod benchmarks { #[divan::bench(args = [OffsetLayout::Zctl, OffsetLayout::Overlapping])] fn compress_listview(bencher: Bencher, layout: OffsetLayout) { let array = build_nested_listview(NUM_ROWS, layout); + mark_optimized(&array, &SESSION).unwrap(); + let nbytes = array.nbytes(); let compressor = BtrBlocksCompressorBuilder::from_session(&SESSION) .unrestricted() diff --git a/vortex-compressor/benches/dict_encode.rs b/vortex-compressor/benches/dict_encode.rs index 25edcb89ce5..a4b1a73ee75 100644 --- a/vortex-compressor/benches/dict_encode.rs +++ b/vortex-compressor/benches/dict_encode.rs @@ -15,6 +15,7 @@ use vortex_array::arrays::PrimitiveArray; use vortex_array::builders::dict::dict_encode; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BufferMut; use vortex_session::VortexSession; @@ -44,6 +45,8 @@ fn make_array() -> PrimitiveArray { #[divan::bench] fn encode_generic(bencher: Bencher) { let array = make_array().into_array(); + mark_optimized(&array, &SESSION).unwrap(); + bencher .with_inputs(|| (&array, SESSION.create_execution_ctx())) .bench_refs(|(array, ctx)| dict_encode(array, ctx).unwrap()); diff --git a/vortex-compressor/benches/float_stats.rs b/vortex-compressor/benches/float_stats.rs index 9fa71ef22f0..f5fa8060b58 100644 --- a/vortex-compressor/benches/float_stats.rs +++ b/vortex-compressor/benches/float_stats.rs @@ -3,6 +3,8 @@ //! Float compression statistics over arrays with and without nulls. +#![expect(clippy::unwrap_used)] + use std::sync::LazyLock; use divan::Bencher; @@ -12,6 +14,7 @@ use vortex_array::VortexSessionExecute; use vortex_array::aggregate_fn::session::AggregateFnSession; use vortex_array::arrays::PrimitiveArray; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_buffer::Buffer; @@ -63,10 +66,10 @@ fn bench_stats(bencher: Bencher, validity: Validity, count_distinct_values: bool let values = values(); bencher .with_inputs(|| { - ( - PrimitiveArray::new(values.clone(), validity.clone()), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::new(values.clone(), validity.clone()); + mark_optimized(array.as_array(), &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(array, ctx)| { FloatStats::generate_opts( diff --git a/vortex-compressor/benches/integer_stats.rs b/vortex-compressor/benches/integer_stats.rs index 068f6f410c0..f6fec32c5b5 100644 --- a/vortex-compressor/benches/integer_stats.rs +++ b/vortex-compressor/benches/integer_stats.rs @@ -3,6 +3,8 @@ //! Integer compression statistics over arrays with and without nulls. +#![expect(clippy::unwrap_used)] + use std::sync::LazyLock; use divan::Bencher; @@ -12,6 +14,7 @@ use vortex_array::VortexSessionExecute; use vortex_array::aggregate_fn::session::AggregateFnSession; use vortex_array::arrays::PrimitiveArray; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::BitBuffer; use vortex_buffer::Buffer; @@ -60,10 +63,10 @@ fn bench_stats(bencher: Bencher, validity: Validity, count_distinct_values: bool let values = values(); bencher .with_inputs(|| { - ( - PrimitiveArray::new(values.clone(), validity.clone()), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::new(values.clone(), validity.clone()); + mark_optimized(array.as_array(), &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(array, ctx)| { IntegerStats::generate_opts( diff --git a/vortex-compressor/benches/stats_calc.rs b/vortex-compressor/benches/stats_calc.rs index 7cad0c37940..0da3abc295f 100644 --- a/vortex-compressor/benches/stats_calc.rs +++ b/vortex-compressor/benches/stats_calc.rs @@ -5,6 +5,7 @@ use mimalloc::MiMalloc; #[cfg(not(codspeed))] #[divan::bench_group(items_count = 64_000u32)] +#[expect(clippy::unwrap_used)] mod benchmarks { use std::sync::LazyLock; @@ -16,6 +17,7 @@ mod benchmarks { use vortex_array::arrays::PrimitiveArray; use vortex_array::dtype::NativePType; use vortex_array::optimizer::kernels::KernelSession; + use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_compressor::stats::GenerateStatsOptions; @@ -86,10 +88,10 @@ mod benchmarks { let values = generate::(distribution); bencher .with_inputs(|| { - ( - PrimitiveArray::new(values.clone(), Validity::NonNullable), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::new(values.clone(), Validity::NonNullable); + mark_optimized(array.as_array(), &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_refs(|(values, ctx)| { IntegerStats::generate_opts( diff --git a/vortex-cuda/benches/alp_cuda.rs b/vortex-cuda/benches/alp_cuda.rs index 4af0ae33d56..7450957f164 100644 --- a/vortex-cuda/benches/alp_cuda.rs +++ b/vortex-cuda/benches/alp_cuda.rs @@ -39,6 +39,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaDispatchMode; use vortex_cuda::CudaSession; use vortex_cuda::executor::CudaArrayExt; @@ -121,6 +122,7 @@ where for &(patch_freq, patch_label) in PATCH_FREQUENCIES { let array = make_alp_array::(len, patch_freq); + mark_optimized(array.as_array(), &vortex_cuda::cuda_session()).unwrap(); group.bench_with_input( BenchmarkId::new(format!("cuda/alp_{}/{}", type_name, patch_label), len_str), diff --git a/vortex-cuda/benches/arrow_binary_cuda.rs b/vortex-cuda/benches/arrow_binary_cuda.rs index 72d46c68683..706f3023c13 100644 --- a/vortex-cuda/benches/arrow_binary_cuda.rs +++ b/vortex-cuda/benches/arrow_binary_cuda.rs @@ -29,6 +29,7 @@ use vortex::dtype::DType; use vortex::dtype::Nullability; use vortex::error::VortexExpect; use vortex::error::VortexResult; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaExecutionCtx; use vortex_cuda::CudaSession; use vortex_cuda::arrow::ArrowDeviceArray; @@ -100,6 +101,8 @@ fn benchmark_arrow_binary_export(c: &mut Criterion) { .with_launch_strategy(Arc::new(timed)); let array = block_on(out_of_line_binary(len, &mut cuda_ctx)) .vortex_expect("failed to create binary fixture"); + mark_optimized(&array, &vortex_cuda::cuda_session()) + .vortex_expect("optimize the benchmark input"); for _ in 0..iters { let mut exported = diff --git a/vortex-cuda/benches/arrow_validity_cuda.rs b/vortex-cuda/benches/arrow_validity_cuda.rs index f7ad0f54061..de6a2b174ed 100644 --- a/vortex-cuda/benches/arrow_validity_cuda.rs +++ b/vortex-cuda/benches/arrow_validity_cuda.rs @@ -35,6 +35,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaExecutionCtx; use vortex_cuda::CudaSession; use vortex_cuda::arrow::ArrowDeviceArray; @@ -135,6 +136,8 @@ fn benchmark_arrow_validity_export(c: &mut Criterion) { &mut cuda_ctx, )) .vortex_expect("failed to create primitive fixture"); + mark_optimized(&array, &SESSION) + .vortex_expect("optimize the benchmark input"); let mut exported_arrays = Vec::with_capacity( usize::try_from(iters) diff --git a/vortex-cuda/benches/bitpacked_cuda.rs b/vortex-cuda/benches/bitpacked_cuda.rs index bffae0ba542..bbbf501724c 100644 --- a/vortex-cuda/benches/bitpacked_cuda.rs +++ b/vortex-cuda/benches/bitpacked_cuda.rs @@ -38,6 +38,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaDispatchMode; use vortex_cuda::CudaSession; use vortex_cuda::executor::CudaArrayExt; @@ -135,6 +136,8 @@ where for &(n_rows, size_str) in bench_config::BENCH_SIZES { let array = make_bitpacked_array::(bit_width, n_rows); + mark_optimized(array.as_array(), &vortex_cuda::cuda_session()).unwrap(); + let nbytes = n_rows * size_of::(); group.throughput(Throughput::Bytes(nbytes as u64)); @@ -190,6 +193,7 @@ where for &(patch_freq, patch_label) in PATCH_FREQUENCIES { let array = make_bitpacked_array_with_patches::(n_rows, patch_freq); + mark_optimized(array.as_array(), &vortex_cuda::cuda_session()).unwrap(); group.bench_with_input( BenchmarkId::new( diff --git a/vortex-cuda/benches/date_time_parts_cuda.rs b/vortex-cuda/benches/date_time_parts_cuda.rs index e77602e4144..1c7d03d67d6 100644 --- a/vortex-cuda/benches/date_time_parts_cuda.rs +++ b/vortex-cuda/benches/date_time_parts_cuda.rs @@ -31,6 +31,7 @@ use vortex::encodings::datetime_parts::DateTimePartsArray; use vortex::error::VortexExpect; use vortex::extension::datetime::TimeUnit; use vortex::extension::datetime::Timestamp; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaDispatchMode; use vortex_cuda::CudaSession; use vortex_cuda::executor::CudaArrayExt; @@ -62,6 +63,7 @@ fn benchmark_datetimeparts(c: &mut Criterion) { let (time_unit, unit_str) = (TimeUnit::Milliseconds, "ms"); let dtp_array = make_datetimeparts_array(len, time_unit); + mark_optimized(dtp_array.as_array(), &vortex_cuda::cuda_session()).unwrap(); group.bench_with_input( BenchmarkId::new(format!("cuda/datetimeparts/{unit_str}"), len_str), diff --git a/vortex-cuda/benches/delta_cuda.rs b/vortex-cuda/benches/delta_cuda.rs index 751d5f79ba0..67d2b77f829 100644 --- a/vortex-cuda/benches/delta_cuda.rs +++ b/vortex-cuda/benches/delta_cuda.rs @@ -39,6 +39,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaDispatchMode; use vortex_cuda::CudaSession; use vortex_cuda::executor::CudaArrayExt; @@ -88,6 +89,7 @@ where group.throughput(Throughput::Bytes((len * size_of::()) as u64)); let delta = make_delta_array::(len); + mark_optimized(delta.as_array(), &vortex_cuda::cuda_session()).unwrap(); group.bench_with_input( BenchmarkId::new(format!("cuda/delta/{type_name}"), len_str), diff --git a/vortex-cuda/benches/dict_cuda.rs b/vortex-cuda/benches/dict_cuda.rs index fee572a4fa8..aaa5d3ebc21 100644 --- a/vortex-cuda/benches/dict_cuda.rs +++ b/vortex-cuda/benches/dict_cuda.rs @@ -27,6 +27,7 @@ use vortex::array::validity::Validity::NonNullable; use vortex::buffer::Buffer; use vortex::dtype::NativePType; use vortex::error::VortexExpect; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaDispatchMode; use vortex_cuda::CudaSession; use vortex_cuda::executor::CudaArrayExt; @@ -84,6 +85,7 @@ where group.throughput(Throughput::Bytes((len * size_of::()) as u64)); let dict_array = make_dict_array_typed::(*len, config.dict_size); + mark_optimized(dict_array.as_array(), &vortex_cuda::cuda_session()).unwrap(); group.bench_with_input( BenchmarkId::new( diff --git a/vortex-cuda/benches/for_cuda.rs b/vortex-cuda/benches/for_cuda.rs index efbaf1fa4b6..f0bae21176d 100644 --- a/vortex-cuda/benches/for_cuda.rs +++ b/vortex-cuda/benches/for_cuda.rs @@ -40,6 +40,7 @@ use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; use vortex_array::session::ArraySession; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaDispatchMode; use vortex_cuda::CudaSession; use vortex_cuda::executor::CudaArrayExt; @@ -102,6 +103,7 @@ where group.throughput(Throughput::Bytes((len * size_of::()) as u64)); let for_array = make_for_array_typed::(len, false); + mark_optimized(for_array.as_array(), &vortex_cuda::cuda_session()).unwrap(); group.bench_with_input( BenchmarkId::new(format!("cuda/for/{type_name}"), len_str), @@ -142,6 +144,7 @@ where group.throughput(Throughput::Bytes((len * size_of::()) as u64)); let for_array = make_for_array_typed::(len, true); + mark_optimized(for_array.as_array(), &vortex_cuda::cuda_session()).unwrap(); group.bench_with_input( BenchmarkId::new(format!("cuda/ffor/{type_name}"), len_str), diff --git a/vortex-cuda/benches/fsst_cuda.rs b/vortex-cuda/benches/fsst_cuda.rs index c0d216bf287..be2b34e539e 100644 --- a/vortex-cuda/benches/fsst_cuda.rs +++ b/vortex-cuda/benches/fsst_cuda.rs @@ -35,6 +35,7 @@ use vortex::encodings::fsst::FSSTArrayExt; use vortex::encodings::fsst::FSSTArraySlotsExt; use vortex::error::VortexExpect; use vortex::session::VortexSession; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaDispatchMode; use vortex_cuda::CudaSession; use vortex_cuda::VarBinExportLayout; @@ -121,8 +122,12 @@ fn make_fixture(n: usize) -> FSSTBenchFixture { .vortex_expect("rebuild FSST fixture with Binary dtype") .into_array(); + let utf8 = fsst.into_array(); + mark_optimized(&utf8, &vortex_cuda::cuda_session()).unwrap(); + mark_optimized(&binary, &vortex_cuda::cuda_session()).unwrap(); + FSSTBenchFixture { - utf8: fsst.into_array(), + utf8, binary, uncompressed_size, } diff --git a/vortex-cuda/benches/list_view_cuda.rs b/vortex-cuda/benches/list_view_cuda.rs index debbaebcf19..5d5f29f485f 100644 --- a/vortex-cuda/benches/list_view_cuda.rs +++ b/vortex-cuda/benches/list_view_cuda.rs @@ -25,6 +25,7 @@ use vortex::array::validity::Validity; use vortex::dtype::PType; use vortex::error::VortexExpect; use vortex::error::VortexResult; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaExecutionCtx; use vortex_cuda::CudaSession; use vortex_cuda::arrow::ArrowDeviceArray; @@ -112,6 +113,8 @@ fn benchmark_list_view_export(c: &mut Criterion) { .with_launch_strategy(Arc::new(timed)); let array = block_on(contiguous_list_view(len, &mut cuda_ctx)) .vortex_expect("failed to create list-view fixture"); + mark_optimized(&array, &vortex_cuda::cuda_session()) + .vortex_expect("optimize the benchmark input"); for _ in 0..iters { let mut exported = @@ -143,6 +146,8 @@ fn benchmark_list_view_export(c: &mut Criterion) { .with_launch_strategy(Arc::new(timed)); let array = block_on(non_contiguous_primitive_list_view(len, &mut cuda_ctx)) .vortex_expect("failed to create list-view fixture"); + mark_optimized(&array, &vortex_cuda::cuda_session()) + .vortex_expect("optimize the benchmark input"); for _ in 0..iters { let mut exported = diff --git a/vortex-cuda/benches/onpair_cuda.rs b/vortex-cuda/benches/onpair_cuda.rs index be80b1441cb..58c4913d04c 100644 --- a/vortex-cuda/benches/onpair_cuda.rs +++ b/vortex-cuda/benches/onpair_cuda.rs @@ -23,6 +23,7 @@ use vortex::array::arrays::VarBinArray; use vortex::dtype::DType; use vortex::dtype::Nullability; use vortex::error::VortexExpect; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaDispatchMode; use vortex_cuda::CudaSession; use vortex_cuda::executor::CudaArrayExt; @@ -61,6 +62,7 @@ fn make_fixture(n: usize) -> OnPairBenchFixture { .into_array(); let array = onpair_compress(&varbin, DEFAULT_CONFIG, setup_ctx.execution_ctx()) .vortex_expect("OnPair compression failed"); + mark_optimized(&array, &vortex_cuda::cuda_session()).unwrap(); OnPairBenchFixture { array, diff --git a/vortex-cuda/benches/runend_cuda.rs b/vortex-cuda/benches/runend_cuda.rs index 8211c2e9a04..df5c1d6f354 100644 --- a/vortex-cuda/benches/runend_cuda.rs +++ b/vortex-cuda/benches/runend_cuda.rs @@ -27,6 +27,7 @@ use vortex::buffer::Buffer; use vortex::dtype::NativePType; use vortex::encodings::runend::RunEnd; use vortex::encodings::runend::RunEndArray; +use vortex_array::test_harness::mark_optimized; use vortex_cuda::CudaDispatchMode; use vortex_cuda::CudaSession; use vortex_cuda::executor::CudaArrayExt; @@ -82,6 +83,7 @@ where let mut cuda_ctx = CudaSession::create_execution_ctx(&vortex_cuda::cuda_session()).unwrap(); let runend_array = make_runend_array_typed::(len, run_len, cuda_ctx.execution_ctx()); + mark_optimized(runend_array.as_array(), &vortex_cuda::cuda_session()).unwrap(); group.bench_with_input( BenchmarkId::new(format!("cuda/runend/{type_name}_runlen_{run_len}"), len_str), diff --git a/vortex-duckdb/benches/decimal_export.rs b/vortex-duckdb/benches/decimal_export.rs index aa9e02757cf..a21becee624 100644 --- a/vortex-duckdb/benches/decimal_export.rs +++ b/vortex-duckdb/benches/decimal_export.rs @@ -29,6 +29,7 @@ use vortex::dtype::NativePType; use vortex::encodings::decimal_byte_parts::DecimalByteParts; use vortex::error::VortexExpect; use vortex::session::VortexSession; +use vortex_array::test_harness::mark_optimized; use vortex_duckdb::_benchmarking::ArrayExporter; use vortex_duckdb::_benchmarking::ConversionCache; use vortex_duckdb::duckdb::DataChunk; @@ -154,6 +155,7 @@ fn decimal_export(bencher: Bencher, case: Case) { .vortex_expect("DuckDB supports the precision of every case"); let array = StructArray::from_fields(&[("value", case.column())]) .vortex_expect("build the exported struct"); + mark_optimized(array.as_array(), &SESSION).vortex_expect("mark the benchmark input"); let cache = ConversionCache::default(); bencher diff --git a/vortex-layout/benches/zone_map_prune.rs b/vortex-layout/benches/zone_map_prune.rs index ebd4ece8218..8291c6e6f20 100644 --- a/vortex-layout/benches/zone_map_prune.rs +++ b/vortex-layout/benches/zone_map_prune.rs @@ -39,6 +39,7 @@ use vortex_array::expr::or; use vortex_array::expr::root; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_expr::stats::rewrite::BoundExpressionPruneExt; @@ -246,6 +247,7 @@ fn build(column_dtype: DType, fields: Vec<(String, ArrayRef)>, num_zones: usize) .collect::>(), ) .unwrap(); + mark_optimized(stats.as_array(), &SESSION).unwrap(); // A trailing short zone, which is the common shape and forces the run-end row-count array. let row_count = ZONE_LEN * (num_zones as u64 - 1) + ZONE_LEN / 2; diff --git a/vortex-row/benches/row_encode.rs b/vortex-row/benches/row_encode.rs index 1795fbc5c00..e9ad9d23cd3 100644 --- a/vortex-row/benches/row_encode.rs +++ b/vortex-row/benches/row_encode.rs @@ -37,6 +37,7 @@ use vortex_array::arrays::VarBinViewArray; use vortex_array::arrays::listview::ListViewArray; use vortex_array::arrays::listview::ListViewArraySlotsExt; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_row::RowEncoder; use vortex_session::VortexSession; @@ -88,7 +89,11 @@ fn make_vortex_list(elements: vortex_array::ArrayRef) -> vortex_array::ArrayRef .into_array(); let list_len = u32::try_from(LIST_LEN).expect("benchmark list length must fit u32"); let sizes = PrimitiveArray::from_iter(std::iter::repeat_n(list_len, N)).into_array(); - ListViewArray::new(elements, offsets, sizes, Validity::NonNullable, &mut ctx).into_array() + let list = + ListViewArray::new(elements, offsets, sizes, Validity::NonNullable, &mut ctx).into_array(); + mark_optimized(&list, &SESSION).unwrap(); + + list } fn arrow_output_bytes(rows: &arrow_row::Rows) -> u64 { @@ -163,6 +168,8 @@ fn print_list_output_sizes() { fn primitive_i64_vortex(bencher: divan::Bencher) { let v = gen_i64(N, 0); let col = PrimitiveArray::from_iter(v.clone()).into_array(); + mark_optimized(&col, &SESSION).unwrap(); + let bytes = (N * (1 + 8)) as u64; let encoder = RowEncoder::default(); bencher @@ -181,6 +188,8 @@ fn utf8_vortex(bencher: divan::Bencher) { .map(|w| 1 + (w.len().div_ceil(32) * 33) as u64) .sum(); let col = VarBinViewArray::from_iter_str(words.iter().map(String::as_str)).into_array(); + mark_optimized(&col, &SESSION).unwrap(); + let encoder = RowEncoder::default(); bencher .counter(BytesCount::new(total)) @@ -211,6 +220,8 @@ fn struct_mixed_vortex(bencher: divan::Bencher) { let struct_arr = StructArray::from_fields(&[("id", id_arr), ("name", name_arr)]) .unwrap() .into_array(); + mark_optimized(&struct_arr, &SESSION).unwrap(); + let encoder = RowEncoder::default(); bencher .counter(BytesCount::new(total)) diff --git a/vortex-spatial/benches/area.rs b/vortex-spatial/benches/area.rs index d6f24e0e222..e2bc0d6a05d 100644 --- a/vortex-spatial/benches/area.rs +++ b/vortex-spatial/benches/area.rs @@ -26,6 +26,7 @@ use vortex_array::dtype::session::DTypeSession; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_arrow::ArrowSession; use vortex_edition::EditionSession; @@ -127,6 +128,8 @@ fn areas(geometry: &ArrayRef, ctx: &mut ExecutionCtx) -> ArrayRef { } fn bench_area(bencher: Bencher, geometry: ArrayRef) { + mark_optimized(&geometry, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) diff --git a/vortex-spatial/benches/binary_predicates.rs b/vortex-spatial/benches/binary_predicates.rs index 6d8eee836a6..d566ba61339 100644 --- a/vortex-spatial/benches/binary_predicates.rs +++ b/vortex-spatial/benches/binary_predicates.rs @@ -39,6 +39,7 @@ use vortex_array::dtype::session::DTypeSession; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_arrow::ArrowSession; use vortex_edition::EditionSession; @@ -110,7 +111,10 @@ fn query_constant(ctx: &mut ExecutionCtx, rows: usize) -> ArrayRef { .unwrap() .execute_scalar(0, ctx) .unwrap(); - ConstantArray::new(scalar, rows).into_array() + let array = ConstantArray::new(scalar, rows).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// A batch-constant point operand with the requested row count. @@ -119,7 +123,10 @@ fn point_constant(ctx: &mut ExecutionCtx, rows: usize) -> ArrayRef { .unwrap() .execute_scalar(0, ctx) .unwrap(); - ConstantArray::new(scalar, rows).into_array() + let array = ConstantArray::new(scalar, rows).into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// A small square (side 2) centered at `(cx, cy)`. @@ -139,7 +146,10 @@ fn squares_mostly_disjoint(rows: usize) -> ArrayRef { let rows = (0..rows) .map(|i| square(150.0 + 700.0 * unit(i), 150.0 + 700.0 * unit(i + 1))) .collect(); - polygon_column(rows).unwrap() + let array = polygon_column(rows).unwrap(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// `rows` small squares whose centers all fall well inside the query polygon, so a bbox check @@ -148,14 +158,20 @@ fn squares_mostly_overlapping(rows: usize) -> ArrayRef { let rows = (0..rows) .map(|i| square(120.0 * unit(i) - 60.0, 120.0 * unit(i + 1) - 60.0)) .collect(); - polygon_column(rows).unwrap() + let array = polygon_column(rows).unwrap(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// `rows` points spread over `[-150, 150)^2`, mixing rows inside and outside the query polygon. fn points(rows: usize) -> ArrayRef { let xs = (0..rows).map(|i| 300.0 * unit(i) - 150.0).collect(); let ys = (0..rows).map(|i| 300.0 * unit(i + 1) - 150.0).collect(); - point_column(xs, ys).unwrap() + let array = point_column(xs, ys).unwrap(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// Execute `array` to completion. @@ -172,18 +188,24 @@ fn execute(array: VortexResult, ctx: &mut ExecutionCtx) -> Array fn nullable_every(array: ArrayRef, null_every: usize) -> ArrayRef { let mut ctx = SESSION.create_execution_ctx(); let validity = Validity::from_iter((0..array.len()).map(|i| !i.is_multiple_of(null_every))); - MaskedArray::try_new(array, validity, &mut ctx) + let array = MaskedArray::try_new(array, validity, &mut ctx) .unwrap() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// A deterministic 90%-null validity pattern. fn ninety_percent_null(array: ArrayRef) -> ArrayRef { let mut ctx = SESSION.create_execution_ctx(); let validity = Validity::from_iter((0..array.len()).map(|i| i.is_multiple_of(10))); - MaskedArray::try_new(array, validity, &mut ctx) + let array = MaskedArray::try_new(array, validity, &mut ctx) .unwrap() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } /// A deterministic 50% validity pattern with a distinct phase per operand. Adjacent phases make @@ -191,9 +213,12 @@ fn ninety_percent_null(array: ArrayRef) -> ArrayRef { fn half_valid(array: ArrayRef, phase: usize) -> ArrayRef { let mut ctx = SESSION.create_execution_ctx(); let validity = Validity::from_iter((0..array.len()).map(|i| (i + phase) % 4 < 2)); - MaskedArray::try_new(array, validity, &mut ctx) + let array = MaskedArray::try_new(array, validity, &mut ctx) .unwrap() - .into_array() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + array } mod contains { diff --git a/vortex-spatial/benches/collect.rs b/vortex-spatial/benches/collect.rs index 2a49c32073b..d08be00a66f 100644 --- a/vortex-spatial/benches/collect.rs +++ b/vortex-spatial/benches/collect.rs @@ -28,6 +28,7 @@ use vortex_array::dtype::session::DTypeSession; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_arrow::ArrowSession; use vortex_edition::EditionSession; @@ -147,6 +148,8 @@ fn collect_list_rows(geometry_lists: &ArrayRef, ctx: &mut ExecutionCtx) -> Array } fn bench_collect(bencher: Bencher, geometry_lists: ArrayRef) { + mark_optimized(&geometry_lists, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -191,6 +194,8 @@ fn envelope_of_collect(input: &ArrayRef, ctx: &mut ExecutionCtx) -> ArrayRef { #[divan::bench] fn envelope_of_collected_points(bencher: Bencher) { let input = point_lists(false); + mark_optimized(&input, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) diff --git a/vortex-spatial/benches/convex_hull.rs b/vortex-spatial/benches/convex_hull.rs index ae9f1a22f95..8d18c653c00 100644 --- a/vortex-spatial/benches/convex_hull.rs +++ b/vortex-spatial/benches/convex_hull.rs @@ -27,6 +27,7 @@ use vortex_array::dtype::session::DTypeSession; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_arrow::ArrowSession; use vortex_edition::EditionSession; @@ -96,6 +97,8 @@ fn hulls(input: &ArrayRef, ctx: &mut ExecutionCtx) -> ArrayRef { } fn bench_hulls(bencher: Bencher, input: ArrayRef) { + mark_optimized(&input, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(input.len())) diff --git a/vortex-spatial/benches/distance.rs b/vortex-spatial/benches/distance.rs index a6484f8ff43..0f5f4e38b67 100644 --- a/vortex-spatial/benches/distance.rs +++ b/vortex-spatial/benches/distance.rs @@ -27,6 +27,7 @@ use vortex_array::dtype::session::DTypeSession; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_arrow::ArrowSession; use vortex_edition::EditionSession; @@ -125,6 +126,9 @@ fn execute(distance: VortexResult, ctx: &mut ExecutionCtx) -> Ar } fn bench_distance(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef) { + mark_optimized(&lhs, &SESSION).unwrap(); + mark_optimized(&rhs, &SESSION).unwrap(); + let rows = lhs.len(); let mut ctx = SESSION.create_execution_ctx(); bencher diff --git a/vortex-spatial/benches/envelope.rs b/vortex-spatial/benches/envelope.rs index 2a8f8b09c59..f55b9a60a68 100644 --- a/vortex-spatial/benches/envelope.rs +++ b/vortex-spatial/benches/envelope.rs @@ -31,6 +31,7 @@ use vortex_array::dtype::session::DTypeSession; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; +use vortex_array::test_harness::mark_optimized; use vortex_arrow::ArrowSession; use vortex_edition::EditionSession; use vortex_edition::EnabledEditions; @@ -99,6 +100,8 @@ fn point_non_nullable(bencher: Bencher) { let xs = (0..ROWS).map(ordinate).collect(); let ys = (0..ROWS).map(|i| ordinate(i + 1)).collect(); let column = point_column(xs, ys).unwrap(); + mark_optimized(&column, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -111,6 +114,8 @@ fn point_mixed_validity(bencher: Bencher) { .map(|i| (!i.is_multiple_of(10)).then(|| (ordinate(i), ordinate(i + 1)))) .collect(); let column = nullable_point_column(points).unwrap(); + mark_optimized(&column, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -123,6 +128,8 @@ fn point_random_nulls(bencher: Bencher) { .map(|i| (!coin(i)).then(|| (ordinate(i), ordinate(i + 1)))) .collect(); let column = nullable_point_column(points).unwrap(); + mark_optimized(&column, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -141,6 +148,8 @@ fn multipoint_non_nullable(bencher: Bencher) { }) .collect(); let column = multipoint_column(rows).unwrap(); + mark_optimized(&column, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -164,6 +173,8 @@ fn multipolygon_row(r: usize) -> MultiPolygonRings { fn multipolygon_non_nullable(bencher: Bencher) { let rows = (0..ROWS).map(multipolygon_row).collect(); let column = multipolygon_column(rows).unwrap(); + mark_optimized(&column, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -176,6 +187,8 @@ fn multipolygon_mixed_validity(bencher: Bencher) { .map(|r| (!r.is_multiple_of(10)).then(|| multipolygon_row(r))) .collect(); let column = nullable_multipolygon_column(rows).unwrap(); + mark_optimized(&column, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -188,6 +201,8 @@ fn multipolygon_random_nulls(bencher: Bencher) { .map(|r| (!coin(r)).then(|| multipolygon_row(r))) .collect(); let column = nullable_multipolygon_column(rows).unwrap(); + mark_optimized(&column, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) diff --git a/vortex-spatial/benches/length.rs b/vortex-spatial/benches/length.rs index 4735d059a16..d7ba227d1d2 100644 --- a/vortex-spatial/benches/length.rs +++ b/vortex-spatial/benches/length.rs @@ -28,6 +28,7 @@ use vortex_array::dtype::session::DTypeSession; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_arrow::ArrowSession; use vortex_edition::EditionSession; @@ -94,6 +95,8 @@ fn lengths(lines: &ArrayRef, ctx: &mut ExecutionCtx) -> ArrayRef { #[divan::bench] fn two_vertex_lines(bencher: Bencher) { let lines = linestrings(2); + mark_optimized(&lines, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -103,6 +106,8 @@ fn two_vertex_lines(bencher: Bencher) { #[divan::bench] fn sixteen_vertex_lines(bencher: Bencher) { let lines = linestrings(16); + mark_optimized(&lines, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -119,6 +124,8 @@ fn nullable_two_vertex_lines(bencher: Bencher) { ) .unwrap() .into_array(); + mark_optimized(&lines, &SESSION).unwrap(); + bencher .counter(ItemsCount::new(ROWS)) .bench_local(|| lengths(&lines, &mut ctx)); diff --git a/vortex-spatial/benches/make_line.rs b/vortex-spatial/benches/make_line.rs index d6225bad97a..1d16161adc0 100644 --- a/vortex-spatial/benches/make_line.rs +++ b/vortex-spatial/benches/make_line.rs @@ -28,6 +28,7 @@ use vortex_array::arrays::ConstantArray; use vortex_array::dtype::session::DTypeSession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar_fn::session::ScalarFnSession; +use vortex_array::test_harness::mark_optimized; use vortex_arrow::ArrowSession; use vortex_edition::EditionSession; use vortex_edition::EnabledEditions; @@ -115,6 +116,9 @@ fn make_lines(starts: &ArrayRef, ends: &ArrayRef, ctx: &mut ExecutionCtx) -> Arr fn column_x_column(bencher: Bencher) { let starts = points(0); let ends = points(97); + mark_optimized(&starts, &SESSION).unwrap(); + mark_optimized(&ends, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) @@ -126,6 +130,9 @@ fn column_x_constant(bencher: Bencher) { let starts = points(0); let mut ctx = SESSION.create_execution_ctx(); let end = point_constant(&mut ctx); + mark_optimized(&starts, &SESSION).unwrap(); + mark_optimized(&end, &SESSION).unwrap(); + bencher .counter(ItemsCount::new(ROWS)) .bench_local(|| make_lines(&starts, &end, &mut ctx)); @@ -135,6 +142,9 @@ fn column_x_constant(bencher: Bencher) { fn nullable_columns(bencher: Bencher) { let starts = nullable_points(0, 8); let ends = nullable_points(97, 11); + mark_optimized(&starts, &SESSION).unwrap(); + mark_optimized(&ends, &SESSION).unwrap(); + let mut ctx = SESSION.create_execution_ctx(); bencher .counter(ItemsCount::new(ROWS)) diff --git a/vortex-tensor/benches/cosine_similarity.rs b/vortex-tensor/benches/cosine_similarity.rs index a600b3c24f7..cbcf92da7db 100644 --- a/vortex-tensor/benches/cosine_similarity.rs +++ b/vortex-tensor/benches/cosine_similarity.rs @@ -30,6 +30,8 @@ use vortex_array::dtype::PType; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; use vortex_array::scalar::Scalar; +use vortex_array::test_harness::mark_decoded; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -95,12 +97,12 @@ fn constant_vector(width: usize) -> ArrayRef { fn bench_cosine(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef) { bencher .with_inputs(|| { - ( - CosineSimilarity::try_new(lhs.clone(), rhs.clone()) - .unwrap() - .into_array(), - SESSION.create_execution_ctx(), - ) + let array = CosineSimilarity::try_new(lhs.clone(), rhs.clone()) + .unwrap() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); } @@ -125,11 +127,16 @@ fn extension_constant_vector(width: usize) -> ArrayRef { .map(|i| Scalar::primitive(((i % 97) as f64) - 48.0, Nullability::NonNullable)) .collect(); let fsl_scalar = Scalar::fixed_size_list(element_dtype, children, Nullability::NonNullable); - ExtensionArray::new( + let array = ExtensionArray::new( ext_dtype, ConstantArray::new(fsl_scalar, ELEMENTS / width).into_array(), ) - .into_array() + .into_array(); + + // The optimizer folds this shape into a constant extension array, but a file can hold it. + mark_decoded(&array); + + array } /// The rhs is the same broadcast query represented as extension-wrapped constant storage. diff --git a/vortex-tensor/benches/inner_product.rs b/vortex-tensor/benches/inner_product.rs index c8471f1ef3e..2de9042dbf8 100644 --- a/vortex-tensor/benches/inner_product.rs +++ b/vortex-tensor/benches/inner_product.rs @@ -24,6 +24,7 @@ use vortex_array::arrays::MaskedArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -72,12 +73,12 @@ fn bench_inner_product(bencher: Bencher, lhs: ArrayRef, rhs: ArrayRef) { bencher .counter(ItemsCount::new(lhs.len())) .with_inputs(|| { - ( - InnerProduct::try_new(lhs.clone(), rhs.clone()) - .unwrap() - .into_array(), - SESSION.create_execution_ctx(), - ) + let array = InnerProduct::try_new(lhs.clone(), rhs.clone()) + .unwrap() + .into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); } diff --git a/vortex-tensor/benches/l2_norm.rs b/vortex-tensor/benches/l2_norm.rs index fe6d2ecbff9..ff45ba09788 100644 --- a/vortex-tensor/benches/l2_norm.rs +++ b/vortex-tensor/benches/l2_norm.rs @@ -24,6 +24,7 @@ use vortex_array::arrays::MaskedArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::memory::MemorySession; use vortex_array::optimizer::kernels::KernelSession; +use vortex_array::test_harness::mark_optimized; use vortex_array::validity::Validity; use vortex_buffer::Buffer; use vortex_session::VortexSession; @@ -70,10 +71,10 @@ fn bench_l2_norm(bencher: Bencher, input: ArrayRef) { bencher .counter(ItemsCount::new(input.len())) .with_inputs(|| { - ( - L2Norm::try_new(input.clone()).unwrap().into_array(), - SESSION.create_execution_ctx(), - ) + let array = L2Norm::try_new(input.clone()).unwrap().into_array(); + mark_optimized(&array, &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_values(|(array, mut ctx)| array.execute::(&mut ctx).unwrap()); } diff --git a/vortex/benches/common_encoding_tree_throughput.rs b/vortex/benches/common_encoding_tree_throughput.rs index 28b62559c49..de446630fd7 100644 --- a/vortex/benches/common_encoding_tree_throughput.rs +++ b/vortex/benches/common_encoding_tree_throughput.rs @@ -61,6 +61,7 @@ use vortex::encodings::runend::RunEndArraySlotsExt; use vortex::error::VortexExpect; use vortex::error::vortex_panic; use vortex::extension::datetime::TimeUnit; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -505,6 +506,8 @@ macro_rules! setup_fn { )] fn decompress(bencher: Bencher, setup_fn: SetupFn) { let compressed = setup_fn(); + mark_optimized(&compressed, &SESSION).unwrap(); + let nbytes = compressed.nbytes(); with_byte_counter(bencher, nbytes) diff --git a/vortex/benches/single_encoding_throughput.rs b/vortex/benches/single_encoding_throughput.rs index ea9d8ad0404..42ff5215b96 100644 --- a/vortex/benches/single_encoding_throughput.rs +++ b/vortex/benches/single_encoding_throughput.rs @@ -49,6 +49,7 @@ use vortex::encodings::zstd; use vortex::encodings::zstd::Zstd; use vortex::encodings::zstd::ZstdData; use vortex_array::VortexSessionExecute; +use vortex_array::test_harness::mark_optimized; use vortex_error::VortexResult; use vortex_sequence::Sequence; use vortex_session::VortexSession; @@ -125,6 +126,11 @@ fn setup_primitive_arrays(len: u64) -> (PrimitiveArray, PrimitiveArray, Primitiv .unwrap() .execute::(&mut ctx) .unwrap(); + + mark_optimized(uint_array.as_array(), &SESSION).unwrap(); + mark_optimized(int_array.as_array(), &SESSION).unwrap(); + mark_optimized(float_array.as_array(), &SESSION).unwrap(); + (uint_array, int_array, float_array) } @@ -171,6 +177,7 @@ fn bench_bitpacked_decompress_u32(bencher: Bencher) { ) .unwrap() .into_array(); + mark_optimized(&compressed, &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 4) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -191,6 +198,7 @@ fn bench_runend_decompress_u32(bencher: Bencher) { let (uint_array, ..) = setup_primitive_arrays(NUM_VALUES); let compressed = RunEnd::encode(uint_array.into_array(), &mut SESSION.create_execution_ctx()).unwrap(); + mark_optimized(compressed.as_array(), &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 4) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -222,6 +230,7 @@ fn bench_delta_decompress_u32(bencher: Bencher) { ) .unwrap() .into_array(); + mark_optimized(&compressed, &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 4) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -236,10 +245,10 @@ fn bench_for_compress_i32(bencher: Bencher) { with_byte_counter(bencher, NUM_VALUES * 4) .with_inputs(|| { - ( - PrimitiveArray::new(values.clone(), validity.clone()), - SESSION.create_execution_ctx(), - ) + let array = PrimitiveArray::new(values.clone(), validity.clone()); + mark_optimized(array.as_array(), &SESSION).unwrap(); + + (array, SESSION.create_execution_ctx()) }) .bench_values(|(a, mut ctx)| FoR::encode(a, &mut ctx).unwrap()); } @@ -248,6 +257,7 @@ fn bench_for_compress_i32(bencher: Bencher) { fn bench_for_decompress_i32(bencher: Bencher) { let (_, int_array, _) = setup_primitive_arrays(NUM_VALUES); let compressed = FoR::encode(int_array, &mut SESSION.create_execution_ctx()).unwrap(); + mark_optimized(compressed.as_array(), &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 4) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -272,6 +282,7 @@ fn bench_dict_decompress_u32(bencher: Bencher) { &mut SESSION.create_execution_ctx(), ) .unwrap(); + mark_optimized(compressed.as_array(), &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 4) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -291,6 +302,7 @@ fn bench_zigzag_compress_i32(bencher: Bencher) { fn bench_zigzag_decompress_i32(bencher: Bencher) { let (_, int_array, _) = setup_primitive_arrays(NUM_VALUES); let compressed = zigzag_encode(int_array.as_view()).unwrap().into_array(); + mark_optimized(&compressed, &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 4) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -301,6 +313,7 @@ fn bench_zigzag_decompress_i32(bencher: Bencher) { #[divan::bench(name = "sequence_compress_u32")] fn bench_sequence_compress_u32(bencher: Bencher) { let seq_array = PrimitiveArray::from_iter(0..NUM_VALUES as u32); + mark_optimized(seq_array.as_array(), &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 4) .with_inputs(|| (seq_array.clone(), SESSION.create_execution_ctx())) @@ -313,6 +326,7 @@ fn bench_sequence_decompress_u32(bencher: Bencher) { let compressed = Sequence::try_new_typed(0, 1, Nullability::NonNullable, NUM_VALUES as usize) .unwrap() .into_array(); + mark_optimized(&compressed, &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 4) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -337,6 +351,7 @@ fn bench_alp_decompress_f64(bencher: Bencher) { &mut SESSION.create_execution_ctx(), ) .unwrap(); + mark_optimized(compressed.as_array(), &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 8) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -358,6 +373,7 @@ fn bench_alp_rd_decompress_f64(bencher: Bencher) { let (_, _, float_array) = setup_primitive_arrays(NUM_VALUES); let encoder = RDEncoder::new(float_array.as_slice::()); let compressed = encoder.encode(float_array.as_view()); + mark_optimized(compressed.as_array(), &SESSION).unwrap(); with_byte_counter(bencher, NUM_VALUES * 8) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -383,6 +399,7 @@ fn bench_pcodec_decompress_f64(bencher: Bencher) { &mut SESSION.create_execution_ctx(), ) .unwrap(); + mark_optimized(compressed.as_array(), &SESSION).unwrap(); with_byte_counter(bencher, PCO_NUM_VALUES * 8) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -419,6 +436,7 @@ fn bench_zstd_decompress_u32(bencher: Bencher) { ) .unwrap() .into_array(); + mark_optimized(&compressed, &SESSION).unwrap(); with_byte_counter(bencher, ZSTD_NUM_VALUES * 4) .with_inputs(|| (&compressed, SESSION.create_execution_ctx())) @@ -432,6 +450,7 @@ fn bench_dict_compress_string(bencher: Bencher) { VarBinViewArray::from_iter_str(gen_varbin_words(STRING_NUM_VALUES, STRING_UNIQUENESS)); let nbytes = varbinview_arr.nbytes(); let array = varbinview_arr.into_array(); + mark_optimized(&array, &SESSION).unwrap(); with_byte_counter(bencher, nbytes) .with_inputs(|| (&array, SESSION.create_execution_ctx())) @@ -447,6 +466,8 @@ fn bench_dict_decompress_string(bencher: Bencher) { &mut SESSION.create_execution_ctx(), ) .unwrap(); + mark_optimized(dict.as_array(), &SESSION).unwrap(); + let nbytes = varbinview_arr.into_array().nbytes(); with_byte_counter(bencher, nbytes) @@ -459,6 +480,8 @@ fn bench_fsst_compress_string(bencher: Bencher) { let varbinview_arr = VarBinViewArray::from_iter_str(gen_varbin_words(STRING_NUM_VALUES, STRING_UNIQUENESS)) .into_array(); + mark_optimized(&varbinview_arr, &SESSION).unwrap(); + let fsst_compressor = fsst_train_compressor(&varbinview_arr, &mut SESSION.create_execution_ctx()).unwrap(); let nbytes = varbinview_arr.nbytes(); @@ -478,6 +501,8 @@ fn bench_fsst_decompress_string(bencher: Bencher) { let fsst_array = fsst_compress(&varbinview_arr, &fsst_compressor, &mut ctx) .unwrap() .into_array(); + mark_optimized(&fsst_array, &SESSION).unwrap(); + let nbytes = varbinview_arr.nbytes(); with_byte_counter(bencher, nbytes) @@ -493,6 +518,8 @@ fn bench_zstd_compress_string(bencher: Bencher) { ZSTD_STRING_UNIQUENESS, )) .into_array(); + mark_optimized(&varbinview_arr, &SESSION).unwrap(); + let nbytes = varbinview_arr.nbytes(); with_byte_counter(bencher, nbytes) @@ -523,6 +550,7 @@ fn bench_zstd_decompress_string(bencher: Bencher) { ) .unwrap() .into_array(); + mark_optimized(&compressed, &SESSION).unwrap(); let nbytes = varbinview_arr.nbytes(); with_byte_counter(bencher, nbytes) diff --git a/vortex/benches/take_union.rs b/vortex/benches/take_union.rs index 8abeea5daa4..f4b6108728c 100644 --- a/vortex/benches/take_union.rs +++ b/vortex/benches/take_union.rs @@ -43,6 +43,7 @@ use vortex::array::validity::Validity; use vortex::buffer::Buffer; use vortex::encodings::sparse; use vortex::encodings::sparse::Sparse; +use vortex_array::test_harness::mark_optimized; use vortex_session::VortexSession; #[global_allocator] @@ -213,6 +214,9 @@ fn random_nullable_indices(rng: &mut StdRng) -> ArrayRef { /// Take `indices` and execute the result, so that the child gathers actually run. fn bench_take(bencher: Bencher, array: ArrayRef, indices: ArrayRef) { + mark_optimized(&array, &SESSION).unwrap(); + mark_optimized(&indices, &SESSION).unwrap(); + bencher .with_inputs(|| (&array, &indices, SESSION.create_execution_ctx())) .bench_refs(|(array, indices, ctx)| { From 5605eab0e343568b89a01c3b924eba26c793d529 Mon Sep 17 00:00:00 2001 From: Joe Isaacs Date: Wed, 4 Nov 2026 14:21:41 +0000 Subject: [PATCH 14/41] Execute filter scalar fns' inputs iteratively with execute_step (#11154) Signed-off-by: Joe Isaacs --- vortex-array/src/executor.rs | 19 +++++ .../src/scalar_fn/fns/between/kernel.rs | 14 ++-- vortex-array/src/scalar_fn/fns/between/mod.rs | 41 +++++++++ vortex-array/src/scalar_fn/fns/binary/mod.rs | 83 +++++++++++++++++++ .../src/scalar_fn/fns/fill_null/mod.rs | 32 +++++++ vortex-array/src/scalar_fn/fns/like/mod.rs | 43 ++++++++++ .../src/scalar_fn/unstable/row/batch/args.rs | 10 +++ vortex-array/src/scalar_fn/vtable.rs | 24 +++++- 8 files changed, 258 insertions(+), 8 deletions(-) diff --git a/vortex-array/src/executor.rs b/vortex-array/src/executor.rs index 86ec920b4c3..71be05d7643 100644 --- a/vortex-array/src/executor.rs +++ b/vortex-array/src/executor.rs @@ -919,6 +919,25 @@ macro_rules! require_child { }}; } +/// The scalar function counterpart of [`require_child!`]: require that input `$idx` of `$args` +/// matches `$M`. If it does not, early-returns a +/// [`ScalarFnExecutionResult`](crate::scalar_fn::ScalarFnExecutionResult) asking the executor to +/// execute that input until it matches, so the function is stepped again with the executed input. +/// +/// ```ignore +/// require_input!(args, 0 => AnyColumnar); +/// ``` +#[macro_export] +macro_rules! require_input { + ($args:expr, $idx:expr => $M:ty) => { + if !$args.input_matches($idx, <$M as $crate::matcher::Matcher>::matches)? { + return Ok($crate::scalar_fn::ScalarFnExecutionResult::execute_input::< + $M, + >($idx)); + } + }; +} + /// Like [`require_child!`], but for optional children. If the child is `None`, this is a no-op. /// If the child is `Some` but does not match `$M`, early-returns an [`ExecutionResult`] requesting /// execution of child `$idx`. diff --git a/vortex-array/src/scalar_fn/fns/between/kernel.rs b/vortex-array/src/scalar_fn/fns/between/kernel.rs index f7a42252164..1772afa7476 100644 --- a/vortex-array/src/scalar_fn/fns/between/kernel.rs +++ b/vortex-array/src/scalar_fn/fns/between/kernel.rs @@ -66,9 +66,10 @@ where let scalar_fn_array = parent .as_opt::() .vortex_expect("ExactScalarFn matcher confirmed ScalarFnArray"); - let children = scalar_fn_array.children(); - let lower = &children[1]; - let upper = &children[2]; + // Read only the bounds: the executor may hand over the parent with the input's slot + // detached while it executes that input. + let lower = scalar_fn_array.get_child(1); + let upper = scalar_fn_array.get_child(2); let arr = array.array().clone(); if let Some(result) = short_circuit(&arr, lower, upper, parent.options)? { return Ok(Some(result)); @@ -101,9 +102,10 @@ where let scalar_fn_array = parent .as_opt::() .vortex_expect("ExactScalarFn matcher confirmed ScalarFnArray"); - let children = scalar_fn_array.children(); - let lower = &children[1]; - let upper = &children[2]; + // Read only the bounds: the executor may hand over the parent with the input's slot + // detached while it executes that input. + let lower = scalar_fn_array.get_child(1); + let upper = scalar_fn_array.get_child(2); let arr = array.array().clone(); if let Some(result) = short_circuit(&arr, lower, upper, parent.options)? { // TODO(joe): return the lazy array directly, blocked on the same executor support as diff --git a/vortex-array/src/scalar_fn/fns/between/mod.rs b/vortex-array/src/scalar_fn/fns/between/mod.rs index 360f609a935..1ec9780c645 100644 --- a/vortex-array/src/scalar_fn/fns/between/mod.rs +++ b/vortex-array/src/scalar_fn/fns/between/mod.rs @@ -13,6 +13,7 @@ use vortex_error::vortex_bail; use vortex_session::VortexSession; use vortex_session::registry::CachedId; +use crate::AnyCanonical; use crate::ArrayRef; use crate::Canonical; use crate::ExecutionCtx; @@ -27,10 +28,12 @@ use crate::dtype::DType::Bool; use crate::dtype::NativePType; use crate::expr::display::ExprDisplay; use crate::proto::expr as pb; +use crate::require_input; use crate::scalar::Scalar; use crate::scalar_fn::Arity; use crate::scalar_fn::ChildName; use crate::scalar_fn::ExecutionArgs; +use crate::scalar_fn::ScalarFnExecutionResult; use crate::scalar_fn::ScalarFnId; use crate::scalar_fn::ScalarFnVTable; use crate::scalar_fn::ScalarFnVTableExt; @@ -343,6 +346,17 @@ impl ScalarFnVTable for Between { between_canonical(&arr, &lower, &upper, options, ctx) } + fn execute_step( + &self, + options: &Self::Options, + args: &dyn ExecutionArgs, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + require_input!(args, 0 => AnyCanonical); + self.execute(options, args, ctx) + .map(ScalarFnExecutionResult::done) + } + fn is_strict(&self, _options: &Self::Options) -> bool { false // NULL AND false = false } @@ -785,4 +799,31 @@ mod tests { assert_eq!(to_int_indices(result, ctx).unwrap(), expected_indices); } + + /// A non-canonical input is executed through the executor before the between kernel runs. + #[test] + fn executes_non_canonical_input_iteratively() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let chunks = vec![ + PrimitiveArray::from_iter([1i32, 5]).into_array(), + PrimitiveArray::from_iter([9i32, 12]).into_array(), + ]; + let dtype = chunks[0].dtype().clone(); + let chunked = crate::arrays::ChunkedArray::try_new(chunks, dtype)?.into_array(); + let result = chunked.between( + ConstantArray::new(5i32, 4).into_array(), + ConstantArray::new(10i32, 4).into_array(), + BetweenOptions { + lower_strict: StrictComparison::NonStrict, + upper_strict: StrictComparison::Strict, + }, + )?; + + assert_arrays_eq!( + result, + BoolArray::from_iter([false, true, true, false]), + &mut ctx + ); + Ok(()) + } } diff --git a/vortex-array/src/scalar_fn/fns/binary/mod.rs b/vortex-array/src/scalar_fn/fns/binary/mod.rs index ee50ac15279..4ccd7be2e80 100644 --- a/vortex-array/src/scalar_fn/fns/binary/mod.rs +++ b/vortex-array/src/scalar_fn/fns/binary/mod.rs @@ -20,19 +20,24 @@ use vortex_error::vortex_bail; use vortex_session::VortexSession; use vortex_session::registry::CachedId; +use crate::AnyColumnar; use crate::ArrayRef; use crate::ExecutionCtx; +use crate::arrays::Constant; use crate::arrays::ScalarFnArray; use crate::dtype::DType; use crate::dtype::Nullability; use crate::expr::BoundExpression; use crate::expr::bound; use crate::expr::display::ExprDisplay; +use crate::matcher::Matcher; use crate::proto::expr as pb; +use crate::require_input; use crate::scalar_fn::Arity; use crate::scalar_fn::ChildName; use crate::scalar_fn::ExecutionArgs; use crate::scalar_fn::ReduceNode; +use crate::scalar_fn::ScalarFnExecutionResult; use crate::scalar_fn::ScalarFnId; use crate::scalar_fn::ScalarFnVTable; use crate::scalar_fn::ScalarFnVTableExt; @@ -315,6 +320,38 @@ impl ScalarFnVTable for Binary { )) } + fn execute_step( + &self, + op: &Operator, + args: &dyn ExecutionArgs, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + let columnar = |idx| args.input_matches(idx, AnyColumnar::matches); + let ready = (columnar(0)? && columnar(1)?) + // A constant operand can decide the result, such as `false AND x` or `x < null`, so + // `execute` does not need the other operand executed. + || match op { + Operator::And | Operator::Or => { + let constant = |idx| args.input_matches(idx, |array| array.is::()); + constant(0)? || constant(1)? + } + _ => { + let null = |idx| { + args.input_matches(idx, |array| { + array.as_constant().is_some_and(|scalar| scalar.is_null()) + }) + }; + null(0)? || null(1)? + } + }; + if !ready { + require_input!(args, 0 => AnyColumnar); + require_input!(args, 1 => AnyColumnar); + } + self.execute(op, args, ctx) + .map(ScalarFnExecutionResult::done) + } + fn is_strict(&self, operator: &Operator) -> bool { !matches!(operator, Operator::And | Operator::Or) } @@ -344,6 +381,7 @@ mod tests { use vortex_session::VortexSession; use super::*; + use crate::Canonical; use crate::IntoArray; use crate::VortexSessionExecute; use crate::arrays::Bool; @@ -750,4 +788,49 @@ mod tests { } Ok(()) } + + /// Comparisons over arithmetic, joined by `AND`: every input is a scalar function, so each + /// operator executes its inputs through the executor. + #[test] + fn executes_non_columnar_inputs_iteratively() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let a = PrimitiveArray::from_iter([1i32, 5, 9, 12]).into_array(); + let b = PrimitiveArray::from_iter([2i32, 2, 2, 2]).into_array(); + let sum = a.binary(b.clone(), Operator::Add)?; + let difference = a.binary(b, Operator::Sub)?; + let ten = ConstantArray::new(10i32, 4).into_array(); + let result = sum + .binary(ten.clone(), Operator::Gt)? + .binary(difference.binary(ten, Operator::Lt)?, Operator::And)?; + + assert_arrays_eq!( + result, + BoolArray::from_iter([false, false, true, false]), + &mut ctx + ); + Ok(()) + } + + /// A constant `false` decides `AND` without executing the other operand, which here fails + /// when executed. + #[test] + fn deciding_constant_skips_other_operand() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let failing = PrimitiveArray::from_iter([1i32, 2, 3]) + .into_array() + .binary(ConstantArray::new(0i32, 3).into_array(), Operator::Div)? + .binary(ConstantArray::new(1i32, 3).into_array(), Operator::Gt)?; + assert!(failing.clone().execute::(&mut ctx).is_err()); + + let result = ConstantArray::new(false, 3) + .into_array() + .binary(failing, Operator::And)?; + + assert_arrays_eq!( + result, + BoolArray::from_iter([false, false, false]), + &mut ctx + ); + Ok(()) + } } diff --git a/vortex-array/src/scalar_fn/fns/fill_null/mod.rs b/vortex-array/src/scalar_fn/fns/fill_null/mod.rs index 7bee3f077ed..e80041f1bc2 100644 --- a/vortex-array/src/scalar_fn/fns/fill_null/mod.rs +++ b/vortex-array/src/scalar_fn/fns/fill_null/mod.rs @@ -27,6 +27,7 @@ use crate::arrays::VarBinView; use crate::dtype::DType; use crate::expr::BoundExpression; use crate::optimizer::ArrayOptimizer; +use crate::require_input; use crate::scalar::Scalar; use crate::scalar_fn::Arity; use crate::scalar_fn::ChildName; @@ -34,6 +35,7 @@ use crate::scalar_fn::EmptyOptions; use crate::scalar_fn::ExecutionArgs; use crate::scalar_fn::ReduceNode; use crate::scalar_fn::ReduceNodeValidity; +use crate::scalar_fn::ScalarFnExecutionResult; use crate::scalar_fn::ScalarFnId; use crate::scalar_fn::ScalarFnVTable; use crate::scalar_fn::ScalarFnVTableExt; @@ -168,6 +170,21 @@ impl ScalarFnVTable for FillNull { } } + fn execute_step( + &self, + options: &Self::Options, + args: &dyn ExecutionArgs, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + // Only once the fill is constant: `execute` materializes a lazy fill first, so the input's + // FillNull kernels can still apply before the input is decoded. + if args.input_matches(1, |fill| fill.is::())? { + require_input!(args, 0 => AnyColumnar); + } + self.execute(options, args, ctx) + .map(ScalarFnExecutionResult::done) + } + fn simplify( &self, _options: &Self::Options, @@ -247,6 +264,7 @@ mod tests { use crate::Canonical; use crate::IntoArray; use crate::VortexSessionExecute; + use crate::arrays::BoolArray; use crate::arrays::Chunked; use crate::arrays::ChunkedArray; use crate::arrays::Constant; @@ -523,4 +541,18 @@ mod tests { let expr = fill_null(get_item("value", root()), lit(0i32)); assert_eq!(expr.to_string(), "vortex.fill_null($.value, 0i32)"); } + + /// A scalar function input is executed through the executor before its nulls are filled. + #[test] + fn executes_non_columnar_input_iteratively() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let input = PrimitiveArray::from_option_iter([Some(1i32), None, Some(3)]) + .into_array() + .binary(ConstantArray::new(2i32, 3).into_array(), Operator::Gt)?; + + let result = input.fill_null(Scalar::from(false))?; + + assert_arrays_eq!(result, BoolArray::from_iter([false, false, true]), &mut ctx); + Ok(()) + } } diff --git a/vortex-array/src/scalar_fn/fns/like/mod.rs b/vortex-array/src/scalar_fn/fns/like/mod.rs index da0d094c201..620cf0bf861 100644 --- a/vortex-array/src/scalar_fn/fns/like/mod.rs +++ b/vortex-array/src/scalar_fn/fns/like/mod.rs @@ -19,6 +19,7 @@ use vortex_error::vortex_err; use vortex_session::VortexSession; use vortex_session::registry::CachedId; +use crate::AnyColumnar; use crate::ArrayRef; use crate::Canonical; use crate::ExecutionCtx; @@ -33,10 +34,12 @@ use crate::dtype::DType; use crate::dtype::Nullability; use crate::expr::display::ExprDisplay; use crate::proto::expr as pb; +use crate::require_input; use crate::scalar::Scalar; use crate::scalar_fn::Arity; use crate::scalar_fn::ChildName; use crate::scalar_fn::ExecutionArgs; +use crate::scalar_fn::ScalarFnExecutionResult; use crate::scalar_fn::ScalarFnId; use crate::scalar_fn::ScalarFnVTable; use crate::scalar_fn::ScalarFnVTableExt; @@ -171,6 +174,25 @@ impl ScalarFnVTable for Like { execute_like(&child, &pattern, *options, ctx) } + fn execute_step( + &self, + options: &Self::Options, + args: &dyn ExecutionArgs, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + // A null pattern decides every row without reading the input. + if !args.input_matches(1, |pattern| { + pattern + .as_constant() + .is_some_and(|pattern| pattern.is_null()) + })? { + require_input!(args, 0 => AnyColumnar); + require_input!(args, 1 => AnyColumnar); + } + self.execute(options, args, ctx) + .map(ScalarFnExecutionResult::done) + } + fn is_strict(&self, _instance: &Self::Options) -> bool { true } @@ -699,4 +721,25 @@ mod tests { let array = VarBinViewArray::from_iter_str(values); assert_eq!(is_ascii(&array.resolved_views()), expected); } + + /// A non-columnar input is executed through the executor before the pattern is matched. + #[test] + fn executes_non_columnar_input_iteratively() -> vortex_error::VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let chunks = vec![ + VarBinViewArray::from_iter_str(["alpha", "beta"]).into_array(), + VarBinViewArray::from_iter_str(["alphabet"]).into_array(), + ]; + let dtype = chunks[0].dtype().clone(); + let chunked = crate::arrays::ChunkedArray::try_new(chunks, dtype)?.into_array(); + let result = Like::try_new( + chunked, + ConstantArray::new("alpha%", 3).into_array(), + LikeOptions::default(), + )? + .into_array(); + + assert_arrays_eq!(result, BoolArray::from_iter([true, false, true]), &mut ctx); + Ok(()) + } } diff --git a/vortex-array/src/scalar_fn/unstable/row/batch/args.rs b/vortex-array/src/scalar_fn/unstable/row/batch/args.rs index 3f9d7ae7d3d..ddbf774b89f 100644 --- a/vortex-array/src/scalar_fn/unstable/row/batch/args.rs +++ b/vortex-array/src/scalar_fn/unstable/row/batch/args.rs @@ -78,4 +78,14 @@ impl ExecutionArgs for BorrowedRowFnArgs<'_> { fn row_count(&self) -> usize { self.row_count } + + fn input_matches(&self, index: usize, predicate: fn(&ArrayRef) -> bool) -> VortexResult { + let input = self.arrays.get(index).ok_or_else(|| { + vortex_err!( + "row-function input index must be less than {}, got {index}", + self.arrays.len(), + ) + })?; + Ok(predicate(input)) + } } diff --git a/vortex-array/src/scalar_fn/vtable.rs b/vortex-array/src/scalar_fn/vtable.rs index 698b4dc2529..307f7f9ad0b 100644 --- a/vortex-array/src/scalar_fn/vtable.rs +++ b/vortex-array/src/scalar_fn/vtable.rs @@ -182,8 +182,9 @@ pub trait ScalarFnVTable: 'static + Sized + Clone + Send + Sync { /// Execute the expression over the input arguments. /// /// Superseded by [`execute_step`](Self::execute_step). Vortex only calls this method through - /// the default `execute_step`, so a function that implements `execute_step` never has - /// `execute` called and should implement it with [`vortex_panic!`]. + /// the default `execute_step`. A function that implements `execute_step` either implements + /// `execute` with [`vortex_panic!`], or keeps its logic here and calls it from `execute_step` + /// once [`require_input!`](crate::require_input) has its inputs in the form it needs. /// /// [`vortex_panic!`]: vortex_error::vortex_panic /// @@ -589,6 +590,14 @@ pub trait ExecutionArgs { /// Returns the row count of the execution scope. fn row_count(&self) -> usize; + + /// Whether the input at the given index satisfies `predicate`. + /// + /// Implementations that hold their inputs override this to test the input in place, without + /// the clone [`get`](Self::get) makes, since it runs on every scalar function execution step. + fn input_matches(&self, index: usize, predicate: fn(&ArrayRef) -> bool) -> VortexResult { + Ok(predicate(&self.get(index)?)) + } } /// The result of a single [`ScalarFnVTable::execute_step`]. @@ -669,6 +678,17 @@ impl ExecutionArgs for VecExecutionArgs { fn row_count(&self) -> usize { self.row_count } + + fn input_matches(&self, index: usize, predicate: fn(&ArrayRef) -> bool) -> VortexResult { + let input = self.inputs.get(index).ok_or_else(|| { + vortex_err!( + "Input index {} out of bounds (num_inputs={})", + index, + self.inputs.len() + ) + })?; + Ok(predicate(input)) + } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] From e390693e4501267b0df2ddd5e64888fd23ce839b Mon Sep 17 00:00:00 2001 From: Joe Isaacs Date: Wed, 4 Nov 2026 14:48:48 +0000 Subject: [PATCH 15/41] Replace layout lowering with a VTable::new_plan method (#11200) Signed-off-by: Joe Isaacs --- vortex-layout/src/layout.rs | 28 +++ vortex-layout/src/layouts/chunked/mod.rs | 26 +++ vortex-layout/src/layouts/dict/mod.rs | 18 ++ vortex-layout/src/layouts/flat/mod.rs | 17 ++ vortex-layout/src/layouts/list/mod.rs | 17 ++ vortex-layout/src/layouts/struct_/mod.rs | 18 ++ vortex-layout/src/layouts/zoned/mod.rs | 54 +++++ vortex-layout/src/plan/physical/lower.rs | 212 ------------------ vortex-layout/src/plan/physical/mod.rs | 2 - .../src/plan/physical/pipeline/tests.rs | 25 +-- vortex-layout/src/plan/physical/slots.rs | 31 +++ vortex-layout/src/plan/physical/tests.rs | 5 +- vortex-layout/src/scan/v2/mod.rs | 3 +- vortex-layout/src/vtable.rs | 15 ++ 14 files changed, 240 insertions(+), 231 deletions(-) delete mode 100644 vortex-layout/src/plan/physical/lower.rs diff --git a/vortex-layout/src/layout.rs b/vortex-layout/src/layout.rs index 5ed2f344847..452043d0db4 100644 --- a/vortex-layout/src/layout.rs +++ b/vortex-layout/src/layout.rs @@ -14,6 +14,7 @@ use vortex_array::dtype::DType; use vortex_array::dtype::FieldName; use vortex_error::VortexExpect; use vortex_error::VortexResult; +use vortex_error::vortex_bail; use vortex_session::VortexSession; use vortex_session::registry::Id; @@ -22,6 +23,7 @@ use crate::LayoutReaderRef; use crate::children::LayoutChildren; use crate::display::DisplayLayoutTree; use crate::display::display_tree_with_segment_sizes; +use crate::plan::physical::PlanRef; use crate::segments::SegmentId; use crate::segments::SegmentSource; use crate::vtable::LayoutRef; @@ -188,6 +190,11 @@ impl Layout { ) -> VortexResult { V::new_reader(self, name, segment_source, session, ctx) } + + /// Construct a physical plan for this layout. + pub fn new_plan(&self) -> VortexResult { + V::new_plan(self) + } } impl Clone for Layout { @@ -268,6 +275,16 @@ pub trait DynLayout: 'static + Send + Sync + Debug { ctx: &LayoutReaderContext, ) -> VortexResult; + /// Constructs a physical plan. + /// + /// The default implementation returns an error: the layout has no physical plan. + fn dyn_new_plan(&self) -> VortexResult { + vortex_bail!( + "No physical plan implementation for layout '{}'", + self.dyn_encoding_id() + ) + } + /// Returns `true` if this layout is indivisible: its readers never register natural split /// boundaries strictly inside their row range (see [`crate::VTable::is_indivisible`]). fn dyn_is_indivisible(&self) -> bool { @@ -330,6 +347,10 @@ impl DynLayout for Layout { Layout::new_reader(self, name, segment_source, session, ctx) } + fn dyn_new_plan(&self) -> VortexResult { + Layout::new_plan(self) + } + fn dyn_is_indivisible(&self) -> bool { self.vtable().is_indivisible() } @@ -430,6 +451,13 @@ impl dyn DynLayout + '_ { self.dyn_new_reader(name, segment_source, session, ctx) } + /// Constructs a physical plan for this layout. + /// + /// The root operator is built immediately; child layouts are planned on first access. + pub fn new_plan(&self) -> VortexResult { + self.dyn_new_plan() + } + /// Returns all serialized (present) children, in slot order. pub fn children(&self) -> VortexResult> { (0..self.nslots()) diff --git a/vortex-layout/src/layouts/chunked/mod.rs b/vortex-layout/src/layouts/chunked/mod.rs index 459429046c2..ae57666a9db 100644 --- a/vortex-layout/src/layouts/chunked/mod.rs +++ b/vortex-layout/src/layouts/chunked/mod.rs @@ -27,6 +27,9 @@ use crate::LayoutRef; use crate::VTable; use crate::children::OwnedLayoutChildren; use crate::layouts::chunked::reader::ChunkedReader; +use crate::plan::physical::ConcatPlan; +use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::segments::SegmentSource; /// Chunked layout vtable. @@ -93,6 +96,29 @@ impl VTable for Chunked { ctx.clone(), ))) } + + fn new_plan(layout: &Layout) -> VortexResult { + let mut row_offsets = Vec::with_capacity(layout.nchildren()); + let mut row_count = 0u64; + for index in 0..layout.nchildren() { + row_offsets.push(row_count); + row_count = row_count + .checked_add(layout.child_row_count(index)) + .ok_or_else(|| vortex_err!("Chunked row count overflow"))?; + } + // SAFETY: Chunked layout construction validates that every child has the parent dtype and + // that their row counts sum to the parent; offsets were computed with checked addition + // above. + let plan = unsafe { + ConcatPlan::from_slots_unchecked( + layout.dtype().clone(), + layout.row_count(), + row_offsets.into(), + PlanSlots::from_layout(&layout.to_layout()), + ) + }; + Ok(plan.into_plan()) + } } impl Layout { diff --git a/vortex-layout/src/layouts/dict/mod.rs b/vortex-layout/src/layouts/dict/mod.rs index 44f57d54222..075f30fb26d 100644 --- a/vortex-layout/src/layouts/dict/mod.rs +++ b/vortex-layout/src/layouts/dict/mod.rs @@ -30,6 +30,9 @@ use crate::LayoutReaderRef; use crate::LayoutRef; use crate::VTable; use crate::children::OwnedLayoutChildren; +use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; +use crate::plan::physical::TakePlan; use crate::segments::SegmentSource; /// Dictionary layout vtable. @@ -127,6 +130,21 @@ impl VTable for Dict { ctx.clone(), )?)) } + + fn new_plan(layout: &Layout) -> VortexResult { + // Dict serialization stores values before codes; the plan order is deliberately codes, + // values because that is the optimizer-facing logical shape. + // SAFETY: Dict layout construction validates its values and codes slots. The plan reorders + // those slots to [codes, values] while preserving the derived output dtype and row domain. + let plan = unsafe { + TakePlan::from_slots_unchecked( + layout.dtype().clone(), + layout.row_count(), + PlanSlots::from_layout_slots(&layout.to_layout(), [1, 0]), + ) + }; + Ok(plan.into_plan()) + } } impl Layout { diff --git a/vortex-layout/src/layouts/flat/mod.rs b/vortex-layout/src/layouts/flat/mod.rs index 5dcc369fcdc..677cc2863f6 100644 --- a/vortex-layout/src/layouts/flat/mod.rs +++ b/vortex-layout/src/layouts/flat/mod.rs @@ -28,6 +28,9 @@ use crate::LayoutReaderRef; use crate::VTable; use crate::children::OwnedLayoutChildren; use crate::layouts::flat::reader::FlatReader; +use crate::plan::physical::FilterPlan; +use crate::plan::physical::PlanRef; +use crate::plan::physical::SegmentScanPlan; use crate::segments::SegmentId; use crate::segments::SegmentSource; @@ -120,6 +123,20 @@ impl VTable for Flat { session.clone(), ))) } + + /// A flat layout plans a [`Filter`](crate::plan::physical::Filter) over its segment scan, so + /// the scan returns only the rows it is executed with. + fn new_plan(layout: &Layout) -> VortexResult { + let scan = SegmentScanPlan::new( + layout.dtype().clone(), + layout.row_count(), + layout.segment_id(), + layout.array_ctx().clone(), + layout.array_tree().cloned(), + ); + // TODO(ji): remove the Filter from the flat layout plan and return the segment scan. + Ok(FilterPlan::new(scan.into_plan()).into_plan()) + } } impl Layout { diff --git a/vortex-layout/src/layouts/list/mod.rs b/vortex-layout/src/layouts/list/mod.rs index 59929e89a03..16d3f2aa9f4 100644 --- a/vortex-layout/src/layouts/list/mod.rs +++ b/vortex-layout/src/layouts/list/mod.rs @@ -33,6 +33,9 @@ use crate::LayoutReaderRef; use crate::LayoutRef; use crate::VTable; use crate::children::OwnedLayoutChildren; +use crate::plan::physical::ListPackPlan; +use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::segments::SegmentSource; /// Child index of the elements layout. @@ -162,6 +165,20 @@ impl VTable for List { ctx, )?)) } + + fn new_plan(layout: &Layout) -> VortexResult { + // SAFETY: List layout construction validates its element, offsets, and optional validity + // child shapes. Its slots are `[elements, offsets, validity]`, which are the ListPack + // slots. + let plan = unsafe { + ListPackPlan::from_slots_unchecked( + layout.dtype().clone(), + layout.row_count(), + PlanSlots::from_layout(&layout.to_layout()), + ) + }; + Ok(plan.into_plan()) + } } impl Layout { diff --git a/vortex-layout/src/layouts/struct_/mod.rs b/vortex-layout/src/layouts/struct_/mod.rs index 4506a0b6e20..510645f57d9 100644 --- a/vortex-layout/src/layouts/struct_/mod.rs +++ b/vortex-layout/src/layouts/struct_/mod.rs @@ -33,6 +33,9 @@ use crate::LayoutReaderRef; use crate::LayoutRef; use crate::VTable; use crate::children::OwnedLayoutChildren; +use crate::plan::physical::PackPlan; +use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; use crate::segments::SegmentSource; /// Struct layout vtable. @@ -123,6 +126,21 @@ impl VTable for Struct { ctx.clone(), )?)) } + + fn new_plan(layout: &Layout) -> VortexResult { + // SAFETY: Struct layout construction validates child count and row counts. Its slots are + // validity then one per field, which are the Pack slots, and its logical slot schema + // defines every field dtype plus the optional non-nullable boolean validity dtype. + let plan = unsafe { + PackPlan::from_slots_unchecked( + layout.struct_fields().clone(), + layout.dtype().nullability(), + layout.row_count(), + PlanSlots::from_layout(&layout.to_layout()), + ) + }; + Ok(plan.into_plan()) + } } impl Layout { diff --git a/vortex-layout/src/layouts/zoned/mod.rs b/vortex-layout/src/layouts/zoned/mod.rs index 6b84d116f9c..e61f8588aa1 100644 --- a/vortex-layout/src/layouts/zoned/mod.rs +++ b/vortex-layout/src/layouts/zoned/mod.rs @@ -45,6 +45,7 @@ use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_error::vortex_ensure; use vortex_error::vortex_ensure_eq; +use vortex_error::vortex_err; use vortex_error::vortex_panic; use vortex_session::VortexSession; use vortex_session::registry::CachedId; @@ -65,6 +66,10 @@ use crate::layouts::zoned::schema::aggregate_specs_from_fns; use crate::layouts::zoned::schema::aggregate_stats_table_dtype; use crate::layouts::zoned::schema::legacy_stats_table_dtype; use crate::layouts::zoned::schema::try_aggregate_fns_from_specs; +use crate::plan::physical::LegacyStatsPlan; +use crate::plan::physical::PlanRef; +use crate::plan::physical::PlanSlots; +use crate::plan::physical::ZonedPlan; use crate::segments::SegmentSource; /// Zoned layout vtable. @@ -174,6 +179,10 @@ impl VTable for Zoned { ) -> VortexResult { layout.zoned_reader(name, segment_source, session, ctx) } + + fn new_plan(layout: &Layout) -> VortexResult { + zoned_plan(layout) + } } impl VTable for LegacyStats { @@ -242,6 +251,51 @@ impl VTable for LegacyStats { ) -> VortexResult { layout.zoned_reader(name, segment_source, session, ctx) } + + fn new_plan(layout: &Layout) -> VortexResult { + zoned_plan(layout) + } +} + +/// Plans a zoned or legacy stats layout. Both share a child shape: transparent data, auxiliary +/// zones. +/// +/// Unoptimized, the plan reads only its data; the optimizer rewrites a statistics predicate over +/// it into a pruning plan. Without zones or statistics it plans the data child directly. +fn zoned_plan>(layout: &Layout) -> VortexResult { + let metadata = layout.data(); + // Without zones, or with a zone table holding no statistics, nothing can be pruned. + let no_stats = metadata + .stats_table_dtype() + .as_struct_fields_opt() + .is_none_or(|fields| fields.nfields() == 0); + if metadata.zone_len() == 0 || no_stats { + let data = layout + .slot(0)? + .ok_or_else(|| vortex_err!("Zoned layout has no data child"))?; + return data.new_plan(); + } + let slots = PlanSlots::from_layout_slots(&layout.to_layout(), [0, 1]); + let zone_len = u64::try_from(metadata.zone_len())?; + // A legacy table is converted to the aggregate schema when the plan is optimized. + Ok(match metadata.legacy_stats() { + Some(stats) => LegacyStatsPlan::from_slots( + layout.dtype().clone(), + layout.row_count(), + slots, + zone_len, + Arc::clone(stats), + ) + .into_plan(), + None => ZonedPlan::from_slots( + layout.dtype().clone(), + layout.row_count(), + slots, + zone_len, + metadata.aggregate_fns(), + ) + .into_plan(), + }) } impl LegacyStatsLayout { diff --git a/vortex-layout/src/plan/physical/lower.rs b/vortex-layout/src/plan/physical/lower.rs deleted file mode 100644 index f496a175914..00000000000 --- a/vortex-layout/src/plan/physical/lower.rs +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -//! Lowering stored layout trees into physical plans. - -use std::sync::Arc; - -use vortex_error::VortexResult; -use vortex_error::vortex_bail; -use vortex_error::vortex_err; - -use crate::LayoutRef; -use crate::layouts::chunked::Chunked; -use crate::layouts::chunked::ChunkedLayout; -use crate::layouts::dict::Dict; -use crate::layouts::dict::DictLayout; -use crate::layouts::flat::Flat; -use crate::layouts::flat::FlatLayout; -use crate::layouts::list::List; -use crate::layouts::list::ListLayout; -use crate::layouts::struct_::Struct; -use crate::layouts::struct_::StructLayout; -use crate::layouts::zoned::LegacyStats; -use crate::layouts::zoned::Zoned; -use crate::plan::physical::ConcatPlan; -use crate::plan::physical::FilterPlan; -use crate::plan::physical::LazyPlanRef; -use crate::plan::physical::LegacyStatsPlan; -use crate::plan::physical::ListPackPlan; -use crate::plan::physical::PackPlan; -use crate::plan::physical::PlanRef; -use crate::plan::physical::PlanSlots; -use crate::plan::physical::SegmentScanPlan; -use crate::plan::physical::TakePlan; -use crate::plan::physical::ZonedPlan; - -/// Lowers `layout` into a physical plan. -/// -/// Zoned and legacy stats layouts keep their zone statistics: unoptimized they read only their -/// data, and the optimizer rewrites a statistics predicate over them into a pruning plan. Those -/// with no zones or no statistics lower to their data. -/// -/// The root operator is built immediately. Each of its slots owns a hidden clone of the source -/// layout and lowers its child independently on first access. -/// -/// A flat layout lowers to a [`Filter`](crate::plan::physical::Filter) over its segment scan, so -/// the scan returns only the rows it is executed with. -pub fn lower(layout: &LayoutRef) -> VortexResult { - if let Some(layout) = layout.as_opt::() { - return Ok(FilterPlan::new(lower_flat(layout).into_plan()).into_plan()); - } - if let Some(layout) = layout.as_opt::() { - return Ok(lower_chunked(layout)?.into_plan()); - } - if let Some(layout) = layout.as_opt::() { - return Ok(lower_struct(layout)?.into_plan()); - } - if let Some(layout) = layout.as_opt::() { - return Ok(lower_dict(layout)?.into_plan()); - } - if let Some(layout) = layout.as_opt::() { - return Ok(lower_list(layout)?.into_plan()); - } - if layout.is::() || layout.is::() { - return lower_zoned(layout); - } - vortex_bail!( - "No physical plan implementation for layout '{}'", - layout.encoding_id() - ) -} - -fn lower_flat(layout: &FlatLayout) -> SegmentScanPlan { - SegmentScanPlan::new( - layout.dtype().clone(), - layout.row_count(), - layout.segment_id(), - layout.array_ctx().clone(), - layout.array_tree().cloned(), - ) -} - -fn lower_chunked(layout: &ChunkedLayout) -> VortexResult { - let mut row_offsets = Vec::with_capacity(layout.nchildren()); - let mut row_count = 0u64; - for index in 0..layout.nchildren() { - row_offsets.push(row_count); - row_count = row_count - .checked_add(layout.child_row_count(index)) - .ok_or_else(|| vortex_err!("Chunked row count overflow"))?; - } - // SAFETY: Chunked layout construction validates that every child has the parent dtype and - // that their row counts sum to the parent; offsets were computed with checked addition above. - Ok(unsafe { - ConcatPlan::from_slots_unchecked( - layout.dtype().clone(), - layout.row_count(), - row_offsets.into(), - lazy_slots(&layout.to_layout()), - ) - }) -} - -fn lower_struct(layout: &StructLayout) -> VortexResult { - // SAFETY: Struct layout construction validates child count and row counts. Its slots are - // validity then one per field, which are the Pack slots, and its logical slot schema defines - // every field dtype plus the optional non-nullable boolean validity dtype. - Ok(unsafe { - PackPlan::from_slots_unchecked( - layout.struct_fields().clone(), - layout.dtype().nullability(), - layout.row_count(), - lazy_slots(&layout.to_layout()), - ) - }) -} - -fn lower_dict(layout: &DictLayout) -> VortexResult { - // Dict serialization stores values before codes; the plan order is deliberately codes, - // values because that is the optimizer-facing logical shape. - // SAFETY: Dict layout construction validates its values and codes slots. The plan reorders - // those slots to [codes, values] while preserving the derived output dtype and row domain. - Ok(unsafe { - TakePlan::from_slots_unchecked( - layout.dtype().clone(), - layout.row_count(), - lazy_slots_from(&layout.to_layout(), [1, 0]), - ) - }) -} - -fn lower_list(layout: &ListLayout) -> VortexResult { - // SAFETY: List layout construction validates its element, offsets, and optional validity - // child shapes. Its slots are `[elements, offsets, validity]`, which are the ListPack slots. - Ok(unsafe { - ListPackPlan::from_slots_unchecked( - layout.dtype().clone(), - layout.row_count(), - lazy_slots(&layout.to_layout()), - ) - }) -} - -/// One lazy plan slot per slot of `layout`, at the same position. -fn lazy_slots(layout: &LayoutRef) -> PlanSlots { - lazy_slots_from(layout, 0..layout.nslots()) -} - -/// One lazy plan slot per entry of `layout_slots`: plan slot `i` lowers layout slot -/// `layout_slots[i]`. -/// -/// This is where every lowering maps layout slots to plan slots. An absent layout slot is an -/// absent plan slot; presence comes from the layout's metadata, so nothing is read or lowered -/// until a plan slot is first accessed. -fn lazy_slots_from(layout: &LayoutRef, layout_slots: impl IntoIterator) -> PlanSlots { - layout_slots - .into_iter() - .map(|slot| { - layout.slot_type(slot)?; - let layout = Arc::clone(layout); - Some(LazyPlanRef::lazy(move || { - let child = layout - .slot(slot)? - .ok_or_else(|| vortex_err!("Layout child slot {slot} is absent"))?; - lower(&child) - })) - }) - .collect() -} - -fn lower_zoned(layout: &LayoutRef) -> VortexResult { - // Zoned and legacy stats layouts share a child shape: transparent data, auxiliary zones. - let metadata = if let Some(layout) = layout.as_opt::() { - layout.data() - } else if let Some(layout) = layout.as_opt::() { - layout.data() - } else { - vortex_bail!("Zoned plan requires a zoned layout") - }; - // Without zones, or with a zone table holding no statistics, nothing can be pruned. - let no_stats = metadata - .stats_table_dtype() - .as_struct_fields_opt() - .is_none_or(|fields| fields.nfields() == 0); - if metadata.zone_len() == 0 || no_stats { - let data = layout - .slot(0)? - .ok_or_else(|| vortex_err!("Zoned layout has no data child"))?; - return lower(&data); - } - let slots = lazy_slots_from(layout, [0, 1]); - let zone_len = u64::try_from(metadata.zone_len())?; - // A legacy table is converted to the aggregate schema when the plan is optimized. - Ok(match metadata.legacy_stats() { - Some(stats) => LegacyStatsPlan::from_slots( - layout.dtype().clone(), - layout.row_count(), - slots, - zone_len, - Arc::clone(stats), - ) - .into_plan(), - None => ZonedPlan::from_slots( - layout.dtype().clone(), - layout.row_count(), - slots, - zone_len, - metadata.aggregate_fns(), - ) - .into_plan(), - }) -} diff --git a/vortex-layout/src/plan/physical/mod.rs b/vortex-layout/src/plan/physical/mod.rs index ae4819ef361..8c578e388aa 100644 --- a/vortex-layout/src/plan/physical/mod.rs +++ b/vortex-layout/src/plan/physical/mod.rs @@ -11,7 +11,6 @@ //! and stages, and [`pipeline`] compiles and runs them. mod display; -mod lower; pub mod operator; pub(crate) mod operators; mod optimize; @@ -28,7 +27,6 @@ pub use display::PlanSummaryExtractor; pub use display::PlanTreeContext; pub use display::PlanTreeDisplay; pub use display::PlanTreeExtractor; -pub use lower::lower; pub use optimize::optimize; pub use plans::Concat; pub use plans::ConcatData; diff --git a/vortex-layout/src/plan/physical/pipeline/tests.rs b/vortex-layout/src/plan/physical/pipeline/tests.rs index c0788a1070d..b4d47073989 100644 --- a/vortex-layout/src/plan/physical/pipeline/tests.rs +++ b/vortex-layout/src/plan/physical/pipeline/tests.rs @@ -57,7 +57,6 @@ use crate::plan::physical::PackPlan; use crate::plan::physical::PlanRef; use crate::plan::physical::SegmentScan; use crate::plan::physical::Take; -use crate::plan::physical::lower; use crate::plan::physical::operator::Cx; use crate::plan::physical::operator::InletSet; use crate::plan::physical::operator::Input; @@ -172,7 +171,7 @@ fn fixture(store: &mut Store) -> VortexResult<(PlanRef, ArrayRef)> { ], ) .into_layout(); - Ok((lower(&layout)?, expected)) + Ok((layout.new_plan()?, expected)) } /// Which outstanding read the scripted IO service completes next. @@ -501,7 +500,7 @@ fn two_columns(store: &mut Store) -> VortexResult<(PlanRef, ArrayRef)> { vec![store.chunked(&a, &[7, 5, 8])?, store.flat(&b)?], ) .into_layout(); - Ok((lower(&layout)?, expected)) + Ok((layout.new_plan()?, expected)) } /// Fields chunked differently are split at their front batch boundaries, in row order, @@ -547,7 +546,7 @@ fn nullable_pack_reads_validity_from_its_first_slot() -> VortexResult<()> { ], ) .into_layout(); - let plan = lower(&layout)?; + let plan = layout.new_plan()?; let rows = 2..18; let mask = Mask::from_iter((0..16).map(|v| v % 4 != 1)); @@ -574,7 +573,7 @@ fn pack_emits_when_every_field_has_data() -> VortexResult<()> { vec![store.chunked(&a, &[1; ROWS as usize])?, store.flat(&b)?], ) .into_layout(); - let plan = lower(&layout)?; + let plan = layout.new_plan()?; let mask = Mask::new_true(ROWS as usize); let order = std::iter::once(ROWS as u32) @@ -611,7 +610,7 @@ fn aligned_fields_stream_one_struct_per_chunk() -> VortexResult<()> { ], ) .into_layout(); - let plan = lower(&layout)?; + let plan = layout.new_plan()?; let mask = Mask::new_true(ROWS as usize); let run = run( @@ -634,7 +633,7 @@ fn aligned_fields_stream_one_struct_per_chunk() -> VortexResult<()> { fn scan_keeps_the_selection_with_or_without_a_filter(#[case] sel: Sel) -> VortexResult<()> { let mut store = Store::default(); let values = PrimitiveArray::from_iter(0..ROWS as i32).into_array(); - let filtered = lower(&store.flat(&values)?)?; + let filtered = store.flat(&values)?.new_plan()?; let scan = filtered .as_opt::() .ok_or_else(|| vortex_err!("A flat layout lowers to a filter"))? @@ -676,7 +675,7 @@ fn take_waits_for_whole_values_and_keeps_them( .into_array(); let codes = PrimitiveArray::from_iter((0..ROWS).map(|v| (v % 3) as u8)).into_array(); let layout = DictLayout::new(store.flat(&values)?, store.flat(&codes)?).into_layout(); - let mut plan = lower(&layout)?; + let mut plan = layout.new_plan()?; let expected = if predicate { BoolArray::from_iter((0..ROWS).map(|row| row % 3 != 0)).into_array() } else { @@ -756,7 +755,7 @@ fn sparse_string_take_preserves_encoded_values(#[case] dtype: DType) -> VortexRe let codes = PrimitiveArray::from_option_iter([Some(1535u32), Some(1), None, Some(0)]).into_array(); let layout = DictLayout::new(store.flat(&values)?, store.flat(&codes)?).into_layout(); - let plan = lower(&layout)?; + let plan = layout.new_plan()?; let mask = Mask::new_true(4); let result = run(&store, &plan, 0..4, mask.clone(), delivery(Delivery::Lifo))?; @@ -809,7 +808,7 @@ fn eval_applies_expression_to_selected_rows() -> VortexResult<()> { fn a_segment_two_readers_need_is_read_by_each() -> VortexResult<()> { let mut store = Store::default(); let a = PrimitiveArray::from_iter(0..ROWS as i32).into_array(); - let column = lower(&store.chunked(&a, &[7, 13])?)?; + let column = store.chunked(&a, &[7, 13])?.new_plan()?; let expected = StructArray::from_fields(&[("x", a.clone()), ("y", a)])?.into_array(); let fields = expected .dtype() @@ -951,8 +950,8 @@ fn bound_selection_matches_projection( fn bound_selection_never_initializes_skipped_chunks() -> VortexResult<()> { let mut store = Store::default(); let array = PrimitiveArray::from_iter(0..20i32).into_array(); - let first = lower(&store.flat(&array.slice(0..5)?)?)?; - let last = lower(&store.flat(&array.slice(15..20)?)?)?; + let first = store.flat(&array.slice(0..5)?)?.new_plan()?; + let last = store.flat(&array.slice(15..20)?)?.new_plan()?; let children = (0..4) .map(|index| { let (first, last) = (first.clone(), last.clone()); @@ -998,7 +997,7 @@ fn bound_selection_keeps_whole_dictionary_values() -> VortexResult<()> { ) .into_layout(); let mask = Mask::from_indices(20, [1, 18]); - let plan = selected(lower(&layout)?, 0..20, mask); + let plan = selected(layout.new_plan()?, 0..20, mask); let result = run( &store, &plan, diff --git a/vortex-layout/src/plan/physical/slots.rs b/vortex-layout/src/plan/physical/slots.rs index 5a9ae1b97d2..32ed269b138 100644 --- a/vortex-layout/src/plan/physical/slots.rs +++ b/vortex-layout/src/plan/physical/slots.rs @@ -11,6 +11,7 @@ use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_error::vortex_err; +use crate::LayoutRef; use crate::plan::physical::PlanRef; type SlotInitializer = dyn Fn() -> VortexResult + 'static + Send + Sync; @@ -79,6 +80,36 @@ impl fmt::Debug for LazyPlanRef { pub struct PlanSlots(SmallVec<[Option; 4]>); impl PlanSlots { + /// One lazy plan slot per slot of `layout`, at the same position. + pub(crate) fn from_layout(layout: &LayoutRef) -> Self { + Self::from_layout_slots(layout, 0..layout.nslots()) + } + + /// One lazy plan slot per entry of `layout_slots`: plan slot `i` plans layout slot + /// `layout_slots[i]`. + /// + /// This is where every layout maps its slots to plan slots. An absent layout slot is an + /// absent plan slot; presence comes from the layout's metadata, so nothing is read or planned + /// until a plan slot is first accessed. + pub(crate) fn from_layout_slots( + layout: &LayoutRef, + layout_slots: impl IntoIterator, + ) -> Self { + layout_slots + .into_iter() + .map(|slot| { + layout.slot_type(slot)?; + let layout = Arc::clone(layout); + Some(LazyPlanRef::lazy(move || { + let child = layout + .slot(slot)? + .ok_or_else(|| vortex_err!("Layout child slot {slot} is absent"))?; + child.new_plan() + })) + }) + .collect() + } + /// Creates empty slots with room for `capacity` slots. pub fn with_capacity(capacity: usize) -> Self { Self(SmallVec::with_capacity(capacity)) diff --git a/vortex-layout/src/plan/physical/tests.rs b/vortex-layout/src/plan/physical/tests.rs index 3ecd7cd2b04..750ac6996a9 100644 --- a/vortex-layout/src/plan/physical/tests.rs +++ b/vortex-layout/src/plan/physical/tests.rs @@ -87,7 +87,7 @@ fn unsupported(row_count: u64, dtype: DType) -> LayoutRef { } fn make_plan(layout: LayoutRef) -> VortexResult { - lower(&layout) + layout.new_plan() } fn make_eval(expression: Expression, child: PlanRef) -> VortexResult { @@ -118,7 +118,8 @@ fn unsupported_layout_has_no_plan() -> VortexResult<()> { let layout = unsupported(3, DType::Null); assert_unsupported( - lower(&layout) + layout + .new_plan() .err() .ok_or_else(|| vortex_err!("unsupported layout unexpectedly produced a plan"))?, ); diff --git a/vortex-layout/src/scan/v2/mod.rs b/vortex-layout/src/scan/v2/mod.rs index 2c401bd46d6..77933bc4126 100644 --- a/vortex-layout/src/scan/v2/mod.rs +++ b/vortex-layout/src/scan/v2/mod.rs @@ -43,7 +43,6 @@ use vortex_session::VortexSession; pub use self::cache::ScanCache; pub use self::cache::WeakScanCache; use crate::LayoutRef; -use crate::plan::physical::lower; use crate::plan::physical::optimize; use crate::plan::physical::plan_row_idx_expression; pub use crate::scan::execution::ChunkPlanner; @@ -169,7 +168,7 @@ impl ScanBuilder { let cache = file.cache.unwrap_or_default(); let root = match cache.root() { Some(root) => root, - None => cache.insert_root(lower(&file.layout)?), + None => cache.insert_root(file.layout.new_plan()?), }; let row_count = root.row_count(); let filtered_chunk_rows = self diff --git a/vortex-layout/src/vtable.rs b/vortex-layout/src/vtable.rs index 7ec27b67046..886f4177e1b 100644 --- a/vortex-layout/src/vtable.rs +++ b/vortex-layout/src/vtable.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use vortex_array::SerializeMetadata; use vortex_array::dtype::DType; use vortex_error::VortexResult; +use vortex_error::vortex_bail; use vortex_session::VortexSession; use crate::DynLayout; @@ -19,6 +20,7 @@ use crate::LayoutId; use crate::LayoutParts; use crate::LayoutReaderContext; use crate::LayoutReaderRef; +use crate::plan::physical::PlanRef; use crate::segments::SegmentId; use crate::segments::SegmentSource; @@ -114,6 +116,19 @@ pub trait VTable: 'static + Clone + Send + Sync + Debug { ctx: &LayoutReaderContext, ) -> VortexResult; + /// Construct a physical plan for this layout. + /// + /// The root operator should be built immediately and its children planned lazily, on first + /// access, so that planning a layout tree reads and plans only what is used. + /// + /// The default implementation returns an error: the layout has no physical plan. + fn new_plan(layout: &Layout) -> VortexResult { + vortex_bail!( + "No physical plan implementation for layout '{}'", + layout.vtable().id() + ) + } + /// Returns `true` if this layout is indivisible: its readers never register natural split /// boundaries strictly inside their row range (see [`crate::LayoutReader::register_splits`]). /// From 463d20344a0ddde899ab19ab178b73701f65086c Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 09:53:04 -0500 Subject: [PATCH 16/41] Balance CodSpeed simulation shards by runtime (#11180) ## Summary The count-based planner fills only 3 of the 5 fixed shard slots and keeps most packages together, although benchmark counts do not predict execution time. Balance recorded target runtimes across all 5 slots, splitting large packages at benchmark-target boundaries. The first CI run reduced the longest shard job from 10m03s to 5m07s. All 5 shards passed. Small packages stay together to limit repeated builds, and allocation-excluded targets remain separate. ## Changes - Record target durations from [develop run 41711616261](https://github.com/vortex-data/vortex/actions/runs/41711616261), commit `24cd0eac8f`, with a command to refresh the snapshot from simulation logs. Measure each complete binary execution, including startup and fixture construction, and use the median when several runs are supplied. - Allocate spare slots to the slower allocation-mode pool and assign the longest targets first. Split packages that exceed a shard's runtime budget or benchmark-count cap. - Preserve the 800-benchmark cap, allocation-mode separation, and complete benchmark binaries. A new target gets a five-second estimate and a warning until its runtime is recorded. - Add coverage for runtime balancing, package splitting, allocation-mode separation, count limits, deterministic output, and duration extraction.
Observed CI shard times | Shard | Reference develop run | This PR | | --- | --- | --- | | 1 | 10m03s | 4m44s | | 2 | 6m13s | 3m43s | | 3 | 2m43s | 4m42s | | 4 | 6s, unused | 5m07s | | 5 | 6s, unused | 2m27s | Compared [run 41711616261](https://github.com/vortex-data/vortex/actions/runs/41711616261) with [run 41716056593](https://github.com/vortex-data/vortex/actions/runs/41716056593) on `863c58f026`. These are elapsed CI job times including setup, builds, measurement, and uploads. Exact timings depend on cache and runner state.
CI passes the planner, all 5 simulation shards, Python formatting, linting, type checks, and all 58 script tests, including the 17 shard-planner tests. Other CI jobs are still running. Executed the planner locally to inspect the proposed assignment. Tests and linting were left to CI. Local hooks were bypassed under the requested CI-only workflow. Signed-off-by: Connor Tsui --- .github/workflows/codspeed.yml | 4 +- scripts/codspeed_bench_durations.json | 173 ++++++++++++++++++++++++++ scripts/codspeed_shards.py | 159 ++++++++++++++++------- scripts/tests/test_codspeed_shards.py | 82 ++++++++++-- 4 files changed, 360 insertions(+), 58 deletions(-) create mode 100644 scripts/codspeed_bench_durations.json diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index c405a4bb861..160902458f3 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -48,8 +48,8 @@ jobs: - ".github/workflows/codspeed.yml" # CodSpeed rejects an upload with too many benchmarks, so the simulation benchmarks run in - # shards of at most 800. The planner reads only Cargo.toml files and the committed benchmark - # counts, so this job needs no Rust toolchain and takes seconds. + # shards of at most 800. The planner balances recorded target runtimes across the available + # slots, keeping allocation modes separate. It reads only manifests and committed estimates. plan-codspeed: name: "Plan Codspeed shards" if: github.event.pull_request.draft != true diff --git a/scripts/codspeed_bench_durations.json b/scripts/codspeed_bench_durations.json new file mode 100644 index 00000000000..9a597a3d746 --- /dev/null +++ b/scripts/codspeed_bench_durations.json @@ -0,0 +1,173 @@ +{ + "source": "https://github.com/vortex-data/vortex/actions/runs/41711616261", + "commit": "24cd0eac8f87c02e990375b30bab3ca4bbf72284", + "seconds": { + "vortex": { + "common_encoding_tree_throughput": 10.125, + "pipeline": 0.287, + "single_encoding_throughput": 11.357, + "take_union": 8.234 + }, + "vortex-alp": { + "alp_compress": 9.783 + }, + "vortex-array": { + "aggregate_grouped": 8.464, + "aggregate_max": 8.12, + "aggregate_sum": 7.173, + "binary_ops": 7.678, + "bool_zip": 2.508, + "cast_decimal": 2.755, + "cast_primitive": 7.48, + "chunk_array_builder": 9.008, + "chunked_dict_builder": 3.757, + "chunked_fsl_canonicalize": 7.35, + "compare": 7.588, + "compare_sorted": 7.584, + "copy_to_canonical_width": 0.293, + "dict_compare": 4.628, + "dict_compress": 4.48, + "dict_mask": 4.735, + "dict_unreferenced_mask": 2.664, + "expr_case_when": 5.711, + "expr_large_struct_pack": 7.823, + "expr_optimize": 7.023, + "filter_bool": 4.714, + "filter_fixed_width": 3.687, + "filter_fsl": 2.558, + "filter_list": 3.578, + "interleave": 3.308, + "kleene_bool": 8.215, + "like": 8.466, + "list_contains_set": 7.751, + "list_length": 7.71, + "list_sum": 9.001, + "listview_builder_extend": 7.729, + "listview_rebuild": 8.04, + "listview_zip": 7.503, + "patches_lookup": 2.696, + "piecewise_sequence_take_primitive": 3.237, + "primitive_zip": 7.668, + "probe": 2.661, + "row_fn_bool_retry": 0.42, + "row_fn_output": 0.42, + "row_fn_utf8_input": 0.418, + "run_end_compress": 9.424, + "run_end_decode": 8.016, + "run_end_decode_primitive": 7.753, + "run_end_filter": 7.925, + "run_end_null_count": 8.231, + "run_end_sum": 2.583, + "run_end_take": 2.796, + "scalar_at_struct": 2.476, + "scalar_construction": 7.346, + "scalar_fn_probe": 7.764, + "scalar_subtract": 0.417, + "search_sorted": 1.13, + "slice_dict_primitive": 7.284, + "take_chunked": 0.321, + "take_filter": 5.251, + "take_fsl": 3.654, + "take_patches": 3.692, + "take_primitive": 3.337, + "take_struct": 2.555, + "take_varbin": 2.399, + "validity_is_valid": 7.238, + "varbinview_compact": 7.362, + "varbinview_fill_null": 7.144, + "varbinview_try_new": 2.679, + "varbinview_zip": 2.526, + "widen": 10.159, + "widen_decimal": 7.503 + }, + "vortex-buffer": { + "allocation": 2.877, + "collect_bool": 2.377, + "cpu_dispatch": 1.269, + "take_bits": 0.297, + "vortex_bitbuffer": 3.676, + "vortex_buffer": 2.864 + }, + "vortex-compressor": { + "dict_encode": 0.425, + "float_stats": 7.996, + "integer_stats": 7.899, + "stats_calc": 0.285 + }, + "vortex-compute": { + "lane_kernels": 6.908 + }, + "vortex-datetime-parts": { + "split_temporal": 7.41 + }, + "vortex-decimal-byte-parts": { + "dbp_assemble": 0.297, + "dbp_compute": 9.177, + "dbp_split": 0.29 + }, + "vortex-elias-fano": { + "unary": 8.611 + }, + "vortex-fastlanes": { + "bitpack_blocked": 0.362, + "bitpack_blocked_decompress": 0.362, + "bitpack_compare": 3.001, + "bitpack_compare_sweep": 6.446, + "bitpacking_filter": 0.363, + "bitpacking_take": 7.35, + "canonicalize_bench": 4.076, + "cast_bitpacked": 3.27, + "compute_between": 3.628, + "for_decode": 0.361, + "for_encode": 0.357 + }, + "vortex-file": { + "split_collection": 7.166 + }, + "vortex-fsst": { + "chunked_dict_fsst_builder": 8.917, + "fsst_compress": 9.615, + "fsst_like": 8.386, + "fsst_take": 12.768, + "fsst_url_compare": 8.272 + }, + "vortex-layout": { + "aggregate_partial_merge": 5.62, + "bloom_filter": 4.933, + "pipeline_scan": 7.054, + "zone_map_prune": 6.858 + }, + "vortex-mask": { + "intersect_by_rank": 2.554, + "mask_iteration": 2.287, + "rank": 2.157, + "valid_counts": 2.02 + }, + "vortex-pco": { + "scalar": 7.955 + }, + "vortex-row": { + "row_encode": 8.766 + }, + "vortex-sparse": { + "sparse_canonical": 7.894, + "sparse_pushdown": 7.672 + }, + "vortex-spatial": { + "area": 13.595, + "binary_predicates": 9.239, + "collect": 5.069, + "convex_hull": 5.064, + "distance": 6.949, + "envelope": 5.535, + "length": 13.364, + "make_line": 12.959, + "predicate_bbox": 0.706 + }, + "vortex-tensor": { + "cosine_similarity": 12.459, + "inner_product": 11.783, + "l2_norm": 5.36 + } + } +} diff --git a/scripts/codspeed_shards.py b/scripts/codspeed_shards.py index f33ed67faf1..039b95d627f 100644 --- a/scripts/codspeed_shards.py +++ b/scripts/codspeed_shards.py @@ -5,21 +5,26 @@ """Plan the CodSpeed simulation shards. CodSpeed rejects an upload with too many benchmarks and recommends fewer than 1,000 per upload, so -the simulation benchmarks are split into shards of at most `CAP` benchmarks each. The plan only -reads `Cargo.toml` files and `codspeed_bench_counts.json`, so it needs no Rust toolchain and runs -in a few seconds before the shard jobs start. +the simulation benchmarks are split into shards of at most `CAP` benchmarks each. Use the available +slots to balance recorded target runtimes, including setup inside each benchmark binary. The plan +reads manifests, benchmark counts, and durations, so it needs no Rust toolchain. python3 scripts/codspeed_shards.py plan >> "$GITHUB_OUTPUT" python3 scripts/codspeed_shards.py counts job-log.txt [...] > scripts/codspeed_bench_counts.json + python3 scripts/codspeed_shards.py durations --source --commit job-log.txt [...] -The second form regenerates the counts from the logs of CodSpeed simulation jobs. +The latter commands regenerate counts and durations from completed simulation job logs. Save the +durations output to `scripts/codspeed_bench_durations.json`. """ import argparse import collections +import datetime +import itertools import json import math import re +import statistics import sys import tomllib from pathlib import Path @@ -27,6 +32,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] COUNTS_PATH = REPO_ROOT / "scripts" / "codspeed_bench_counts.json" +DURATIONS_PATH = REPO_ROOT / "scripts" / "codspeed_bench_durations.json" # Most benchmarks per shard, leaving headroom under CodSpeed's recommended 1,000. CAP = 800 @@ -38,6 +44,9 @@ # Benchmarks measured for a target missing from the counts file, until the file is regenerated. DEFAULT_COUNT = 100 +# A new target starts with a five-second estimate until simulation logs record its runtime. +DEFAULT_SECONDS = 5.0 + # The packages whose benchmarks run in simulation, with the features each one builds with. PACKAGES: dict[str, tuple[str, ...]] = { "vortex": (), @@ -71,7 +80,14 @@ # One entry of the GitHub Actions matrix. Shard = TypedDict( "Shard", - {"shard": int, "benchmarks": int, "packages": str, "build-args": str, "exclude-allocations": bool}, + { + "shard": int, + "benchmarks": int, + "seconds": float, + "packages": str, + "build-args": str, + "exclude-allocations": bool, + }, ) @@ -104,49 +120,65 @@ def load_counts(path: Path = COUNTS_PATH) -> dict[Target, int]: return {(package, name): count for package, benches in raw.items() for name, count in benches.items()} -def pack(items: list[tuple[str, list[Target], int]], cap: int) -> list[list[tuple[str, list[Target], int]]]: - """Pack `(key, targets, count)` items into as few bins of at most `cap` as greedy allows. +def load_durations(path: Path = DURATIONS_PATH) -> dict[Target, float]: + raw = json.loads(path.read_text())["seconds"] + return {(package, name): seconds for package, benches in raw.items() for name, seconds in benches.items()} - Largest item first, each into the currently lightest bin; a new bin opens only when the - lightest one cannot take the item. - """ - total = sum(count for _, _, count in items) - bins: list[list[tuple[str, list[Target], int]]] = [[] for _ in range(max(1, math.ceil(total / cap)))] - for item in sorted(items, key=lambda item: (-item[2], item[0])): - lightest = min(bins, key=lambda b: sum(count for _, _, count in b)) - if sum(count for _, _, count in lightest) + item[2] > cap: - lightest = [] - bins.append(lightest) - lightest.append(item) - return [b for b in bins if b] - - -def plan(counts: dict[Target, int], cap: int = CAP) -> list[Shard]: - """Split targets into shards of at most `cap` benchmarks. - - Targets that exclude allocations and the rest are packed separately. Within each pool, a - package's targets stay together unless the package alone exceeds `cap`, in which case its - targets are packed one by one. + +def plan( + counts: dict[Target, int], + cap: int = CAP, + durations: dict[Target, float] | None = None, + max_shards: int = MAX_SHARDS, +) -> list[Shard]: + """Balance target runtimes across available slots, with at most `cap` benchmarks per shard. + + Allocation modes stay separate. Keep small packages together to limit repeated builds, but + split a package above the per-shard runtime budget or count cap at target boundaries. Keep + each target intact because its benchmarks share process state. """ for target, count in counts.items(): if count > cap: raise SystemExit(f"{target[0]}::{target[1]} alone has {count} benchmarks; split it below {cap}") - pools: dict[bool, dict[str, list[Target]]] = collections.defaultdict(lambda: collections.defaultdict(list)) + durations = durations or {} + seconds = {target: durations.get(target, DEFAULT_SECONDS) for target in counts} + pools: dict[bool, list[Target]] = collections.defaultdict(list) for target in sorted(counts): - pools[excludes_allocations(target)][target[0]].append(target) + pools[excludes_allocations(target)].append(target) + + slots = {exclude: max(1, math.ceil(sum(counts[t] for t in targets) / cap)) for exclude, targets in pools.items()} + if sum(slots.values()) > max_shards: + raise SystemExit(f"benchmark counts require more than {max_shards} shard slots") + while sum(slots.values()) < max_shards: + candidates = [exclude for exclude in sorted(pools) if slots[exclude] < len(pools[exclude])] + if not candidates: + break + exclude = max(candidates, key=lambda mode: sum(seconds[t] for t in pools[mode]) / slots[mode]) + slots[exclude] += 1 shards: list[Shard] = [] for exclude in sorted(pools): + budget = sum(seconds[t] for t in pools[exclude]) / slots[exclude] items = [] - for package, targets in sorted(pools[exclude].items()): - total = sum(counts[t] for t in targets) - if total <= cap: - items.append((package, targets, total)) + for _, group in itertools.groupby(pools[exclude], key=lambda t: t[0]): + targets = list(group) + if sum(seconds[t] for t in targets) <= budget and sum(counts[t] for t in targets) <= cap: + items.append(targets) else: - items.extend((f"{t[0]}::{t[1]}", [t], counts[t]) for t in targets) - for shard_items in pack(items, cap): - targets = sorted(t for _, ts, _ in shard_items for t in ts) + items.extend([target] for target in targets) + bins: list[list[Target]] = [[] for _ in range(slots[exclude])] + for item in sorted(items, key=lambda ts: (-sum(seconds[t] for t in ts), -sum(counts[t] for t in ts), ts)): + count = sum(counts[t] for t in item) + candidates = [b for b in bins if sum(counts[t] for t in b) + count <= cap] + if not candidates: + raise SystemExit(f"cannot pack benchmarks into {max_shards} shard slots under the {cap}-benchmark cap") + lightest = min(candidates, key=lambda b: sum(seconds[t] for t in b)) + lightest.extend(item) + for targets in bins: + if not targets: + continue + targets = sorted(targets) packages = sorted({package for package, _ in targets}) features = sorted(f"{package}/{feature}" for package in packages for feature in PACKAGES[package]) args = [f"--features {','.join(features)}"] if features else [] @@ -156,16 +188,12 @@ def plan(counts: dict[Target, int], cap: int = CAP) -> list[Shard]: { "shard": len(shards) + 1, "benchmarks": sum(counts[t] for t in targets), + "seconds": round(sum(seconds[t] for t in targets), 3), "packages": " ".join(packages), "build-args": " ".join(args), "exclude-allocations": exclude, } ) - if len(shards) > MAX_SHARDS: - raise SystemExit( - f"{len(shards)} shards needed but the workflow has {MAX_SHARDS} slots; add slots to the " - "`bench-codspeed` matrix and MAX_SHARDS together" - ) return shards @@ -197,12 +225,37 @@ def counts_from_logs(paths: list[Path]) -> dict[str, dict[str, int]]: return {package: dict(sorted(benches.items())) for package, benches in sorted(counts.items())} +def durations_from_logs(paths: list[Path]) -> dict[str, dict[str, float]]: + """Measure complete target executions, including startup and untimed fixture construction.""" + samples: dict[Target, list[float]] = collections.defaultdict(list) + for path in paths: + current = None + started = None + for line in path.read_text(errors="replace").splitlines(): + if match := re.search(r"\bRunning (\S+) (\S+)$", line): + current = (match.group(1), match.group(2)) if match.group(1) in PACKAGES else None + if current: + started = datetime.datetime.fromisoformat(line.split()[0]) + elif current and started is not None and re.search(rf"\bDone running {re.escape(current[1])}$", line): + ended = datetime.datetime.fromisoformat(line.split()[0]) + samples[current].append((ended - started).total_seconds()) + current = None + durations: dict[str, dict[str, float]] = collections.defaultdict(dict) + for (package, target), values in sorted(samples.items()): + durations[package][target] = round(statistics.median(values), 3) + return dict(durations) + + def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = parser.add_subparsers(dest="command", required=True) sub.add_parser("plan", help="print the shard plan as GITHUB_OUTPUT lines") counts_parser = sub.add_parser("counts", help="regenerate benchmark counts from CodSpeed job logs") counts_parser.add_argument("logs", nargs="+", type=Path) + durations_parser = sub.add_parser("durations", help="record target runtimes from completed simulation job logs") + durations_parser.add_argument("--source", required=True, help="workflow run URL") + durations_parser.add_argument("--commit", required=True, help="measured commit SHA") + durations_parser.add_argument("logs", nargs="+", type=Path) args = parser.parse_args() if args.command == "counts": @@ -210,7 +263,17 @@ def main() -> None: sys.stdout.write("\n") return + if args.command == "durations": + json.dump( + {"source": args.source, "commit": args.commit, "seconds": durations_from_logs(args.logs)}, + sys.stdout, + indent=2, + ) + sys.stdout.write("\n") + return + known = load_counts() + durations = load_durations() counts = {} for target in discover_targets(): if target not in known: @@ -220,9 +283,19 @@ def main() -> None: file=sys.stderr, ) counts[target] = known.get(target, DEFAULT_COUNT) - shards = plan(counts) + if target not in durations: + print( + f"::warning::{target[0]}::{target[1]} is missing from {DURATIONS_PATH.name}; " + f"assuming {DEFAULT_SECONDS}s. Regenerate durations from simulation job logs.", + file=sys.stderr, + ) + shards = plan(counts, durations=durations) for shard in shards: - print(f"shard {shard['shard']}: {shard['benchmarks']} benchmarks, {shard['packages']}", file=sys.stderr) + print( + f"shard {shard['shard']}: {shard['benchmarks']} benchmarks, " + f"{shard['seconds']:.1f}s estimated execution, {shard['packages']}", + file=sys.stderr, + ) for name, value in github_outputs(shards).items(): print(f"{name}={value}") diff --git a/scripts/tests/test_codspeed_shards.py b/scripts/tests/test_codspeed_shards.py index b753362ba4d..c254c0ba00d 100644 --- a/scripts/tests/test_codspeed_shards.py +++ b/scripts/tests/test_codspeed_shards.py @@ -37,7 +37,7 @@ def _targets(shard: dict[str, object]) -> set[str]: def test_every_target_lands_in_exactly_one_shard_under_the_cap(): counts = shards.load_counts() - plan = shards.plan(counts) + plan = shards.plan(counts, durations=shards.load_durations()) seen = [name for shard in plan for name in _targets(shard)] assert sorted(seen) == sorted({name for _, name in counts}) assert len(seen) == len(set(seen)) @@ -54,28 +54,57 @@ def test_allocator_exclusion_never_shares_a_shard(): } plan = shards.plan(counts, cap=100) excluded = [s for s in plan if s["exclude-allocations"]] - assert [_targets(s) for s in excluded] == [{"take_primitive", "dict_compress"}] + assert set().union(*(_targets(s) for s in excluded)) == {"take_primitive", "dict_compress"} assert all(not (_targets(s) & {"take_primitive", "dict_compress"}) for s in plan if not s["exclude-allocations"]) -def test_package_stays_together_under_the_cap_and_splits_over_it(): - counts = { - ("vortex-buffer", "a"): 30, - ("vortex-buffer", "b"): 30, - ("vortex-fastlanes", "c"): 60, - ("vortex-fastlanes", "d"): 60, - ("vortex-mask", "e"): 50, +def test_runtime_balancing_splits_a_package_even_below_the_count_cap(): + counts = {("vortex-buffer", name): 1 for name in "abcd"} + durations = { + ("vortex-buffer", name): seconds for name, seconds in zip("abcd", [100.0, 90.0, 10.0, 1.0], strict=True) } - plan = shards.plan(counts, cap=100) + plan = shards.plan(counts, cap=100, durations=durations, max_shards=2) + assert [_targets(s) for s in plan] == [{"a", "d"}, {"b", "c"}] + assert [s["seconds"] for s in plan] == [101, 100] + + +def test_spare_slots_go_to_the_slower_allocation_pool(): + counts = {("vortex-buffer", name): 1 for name in "abcd"} + counts[("vortex-array", "take_x")] = 1 + durations = {target: 40.0 for target in counts} + durations[("vortex-array", "take_x")] = 10.0 + plan = shards.plan(counts, durations=durations) + assert len(plan) == shards.MAX_SHARDS + assert sum(not s["exclude-allocations"] for s in plan) == 4 + assert [_targets(s) for s in plan if s["exclude-allocations"]] == [{"take_x"}] + + +def test_small_packages_stay_together_when_they_fit_the_runtime_budget(): + counts = {("vortex-buffer", name): 1 for name in "ab"} + counts.update({("vortex-fastlanes", name): 1 for name in "cd"}) + durations = {target: 10.0 if target[0] == "vortex-buffer" else 20.0 for target in counts} + plan = shards.plan(counts, durations=durations, max_shards=2) by_target = {name: s["shard"] for s in plan for name in _targets(s)} assert by_target["a"] == by_target["b"] assert by_target["c"] != by_target["d"] - assert all(s["benchmarks"] <= 100 for s in plan) + + +def test_runtime_balancing_respects_the_count_cap(): + counts = {("vortex-buffer", name): 60 for name in "abc"} + durations = {target: 1.0 for target in counts} + plan = shards.plan(counts, cap=100, durations=durations, max_shards=3) + assert len(plan) == 3 + assert all(s["benchmarks"] == 60 for s in plan) + + +def test_new_target_uses_the_runtime_estimate(): + (shard,) = shards.plan({("vortex-buffer", "new"): 1}, durations={}) + assert shard["seconds"] == shards.DEFAULT_SECONDS def test_build_args_name_only_the_features_of_selected_packages(): counts = {("vortex-buffer", "a"): 1, ("vortex-fsst", "b"): 1} - (shard,) = shards.plan(counts, cap=100) + (shard,) = shards.plan(counts, cap=100, max_shards=1) assert ( shard["build-args"] == "--features vortex-fsst/_test-harness -p vortex-buffer -p vortex-fsst --bench a --bench b" @@ -114,7 +143,10 @@ def test_workflow_matrix_has_max_shards_slots(): def test_plan_is_deterministic(): counts = shards.load_counts() - assert json.dumps(shards.plan(counts)) == json.dumps(shards.plan(dict(reversed(list(counts.items()))))) + durations = shards.load_durations() + assert json.dumps(shards.plan(counts, durations=durations)) == json.dumps( + shards.plan(dict(reversed(list(counts.items()))), durations=dict(reversed(list(durations.items())))) + ) def test_counts_file_covers_every_discovered_target(): @@ -133,3 +165,27 @@ def test_counts_from_logs(tmp_path: Path): "2026-01-01T00:00:00.0Z Measured: ignored\n" ) assert shards.counts_from_logs([log]) == {"vortex-buffer": {"buffer": 2}} + + +def test_durations_include_the_whole_target_and_ignore_incomplete_runs(tmp_path: Path): + log = tmp_path / "job.txt" + log.write_text( + "2026-01-01T00:00:00.0000000Z Running vortex-buffer buffer\n" + "2026-01-01T00:00:02.0000000Z Measured: vortex-buffer/benches/buffer.rs::a\n" + "2026-01-01T00:00:05.5000000Z Done running buffer\n" + "2026-01-01T00:00:06.0000000Z Running post-execution phase...\n" + "2026-01-01T00:00:07.0000000Z Running vortex-array widen\n" + ) + assert shards.durations_from_logs([log]) == {"vortex-buffer": {"buffer": 5.5}} + + +def test_durations_use_the_median_of_complete_runs(tmp_path: Path): + logs = [] + for seconds in [2, 4, 30]: + log = tmp_path / f"{seconds}.txt" + log.write_text( + "2026-01-01T00:00:00.0000000Z Running vortex-datetime-parts split_temporal\n" + f"2026-01-01T00:00:{seconds:02}.0000000Z Done running split_temporal\n" + ) + logs.append(log) + assert shards.durations_from_logs(logs) == {"vortex-datetime-parts": {"split_temporal": 4.0}} From fdd96d09a2b8b247678e23e80ab3e85e397bbc25 Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Wed, 4 Nov 2026 14:56:42 +0000 Subject: [PATCH 17/41] Faster Spark reads and smaller JNI and stats fixes (#11109) - Spark columns read their values directly from the Arrow buffers: validity bits, fixed-width values, string offsets and string views. A decimal of 18 digits or fewer is returned as its unscaled long, as Spark's own vectorized readers do. - `ArrowProperties` turns off the bounds and null checks of the relocated Arrow on each value, as Spark does for its own Arrow, unless the user sets them. - Readers get the Hadoop configuration through a broadcast (`VortexIo.broadcast`), instead of with every task. - `IN` is pushed down as a native set instead of an `OR` of equalities. A null element stays in the set, so `NOT IN` keeps the SQL result. - The JNI writer takes ownership of the Arrow schemas it receives and releases them, so `writeBatch` no longer leaks the exported schema. The Java release added in #11081 now runs only when native code fails before it takes the schema. - File statistics and zone maps skip variants nested in structs, lists and maps, which no builder can hold. Signed-off-by: Robert Kruszewski --- .../java/dev/vortex/api/VortexWriter.java | 7 +- .../java/dev/vortex/jni/JNIWriterTest.java | 31 +++ .../dev/vortex/spark/ArrowProperties.java | 38 ++++ .../dev/vortex/spark/VortexSparkSession.java | 5 + .../java/dev/vortex/spark/io/VortexIo.java | 33 ++- .../read/SparkFilterToVortexExpression.java | 30 ++- .../read/VortexAggregateReaderFactory.java | 6 + .../spark/read/VortexArrowColumnVector.java | 214 ++++++++++++------ .../read/VortexPartitionReaderFactory.java | 6 + .../write/VortexOutputWriterFactory.java | 6 + .../dev/vortex/spark/VortexFileFormat.scala | 6 +- .../dev/vortex/spark/read/VortexScan.scala | 10 +- .../spark/VortexFilterPushdownTest.java | 9 + .../SparkFilterToVortexExpressionTest.java | 5 +- .../spark/read/VortexDecimalAccessorTest.java | 8 +- vortex-jni/src/writer.rs | 19 +- vortex-layout/src/layouts/file_stats.rs | 33 ++- vortex-layout/src/layouts/zoned/builder.rs | 18 +- vortex-layout/src/layouts/zoned/writer.rs | 19 ++ 19 files changed, 401 insertions(+), 102 deletions(-) create mode 100644 java/vortex-spark/common/src/main/java/dev/vortex/spark/ArrowProperties.java diff --git a/java/vortex-jni/src/main/java/dev/vortex/api/VortexWriter.java b/java/vortex-jni/src/main/java/dev/vortex/api/VortexWriter.java index 8dca6b67a81..29c76d8caa6 100644 --- a/java/vortex-jni/src/main/java/dev/vortex/api/VortexWriter.java +++ b/java/vortex-jni/src/main/java/dev/vortex/api/VortexWriter.java @@ -171,7 +171,7 @@ public VortexWriter build() throws IOException { } return new VortexWriter(pointer, arrowSchema); } finally { - // Native code only borrows the schema, so the children the export allocated are ours to release. + // Native code takes and releases the schema. If it failed before it took the schema, release it here. if (ffi.snapshot().release != 0) { ffi.release(); } @@ -216,8 +216,9 @@ public synchronized void writeBatch(VectorSchemaRoot root, BufferAllocator alloc } /** - * Write a batch directly from Arrow C Data Interface addresses. These buffers share the producer's batch release - * callback; use {@link #writeBatch(VectorSchemaRoot, BufferAllocator)} for independently retained Java allocations. + * Write a batch directly from Arrow C Data Interface addresses. Native code takes both the array and the schema, + * and releases them. These buffers share the producer's batch release callback; use + * {@link #writeBatch(VectorSchemaRoot, BufferAllocator)} for independently retained Java allocations. */ public synchronized void writeBatch(long arrowArrayAddr, long arrowSchemaAddr) throws IOException { Preconditions.checkState(!closed.get(), "writer already closed"); diff --git a/java/vortex-jni/src/test/java/dev/vortex/jni/JNIWriterTest.java b/java/vortex-jni/src/test/java/dev/vortex/jni/JNIWriterTest.java index aed1c092a33..9248786824c 100644 --- a/java/vortex-jni/src/test/java/dev/vortex/jni/JNIWriterTest.java +++ b/java/vortex-jni/src/test/java/dev/vortex/jni/JNIWriterTest.java @@ -148,6 +148,37 @@ private static void populateParquetVariantRoot(VectorSchemaRoot root) { root.setRowCount(3); } + @Test + public void testWriterReleasesEveryExportedSchema(@TempDir Path dir) throws IOException { + Session session = Session.create(); + Schema schema = new Schema(List.of(Field.notNullable("v", new ArrowType.Int(64, true)))); + // A dedicated allocator shows what the native writer still holds once the file is finished. + try (RootAllocator allocator = new RootAllocator()) { + try (VortexWriter writer = VortexWriter.builder( + session, dir.resolve("released.vortex").toString(), schema, allocator) + .build(); + VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) { + for (int batch = 0; batch < 5; batch++) { + BigIntVector values = (BigIntVector) root.getVector(0); + values.allocateNew(1024); + for (int i = 0; i < 1024; i++) { + values.set(i, i + batch); + } + root.setRowCount(1024); + try (ArrowArray array = ArrowArray.allocateNew(allocator); + ArrowSchema arrowSchema = ArrowSchema.allocateNew(allocator)) { + Data.exportVectorSchemaRoot(allocator, root, null, array, arrowSchema); + writer.writeBatch(array.memoryAddress(), arrowSchema.memoryAddress()); + } + root.clear(); + } + writer.finish(); + } + // Native code is the consumer of every exported array and schema, so it must have released them all. + assertEquals(0, allocator.getAllocatedMemory()); + } + } + @Test public void testCreateWriter() throws IOException { Path outputPath = tempDir.resolve("test_create.vortex"); diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/ArrowProperties.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/ArrowProperties.java new file mode 100644 index 00000000000..35b1569b235 --- /dev/null +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/ArrowProperties.java @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +package dev.vortex.spark; + +/** + * Turns off the per-value checks of the Arrow that Vortex relocates, as Spark's vectorized readers do for their own + * Arrow. + * + *

Arrow reads these settings once, when it first loads the classes that check bounds and nulls. So the Vortex + * classes that load first in a JVM call {@link #configure()} from their static initializers. A setting the user gives + * as a system property or an environment variable stays as it is. + * + *

The readers check bounds and nulls themselves: Spark asks a column for null before it asks for a value, and each + * value is read at an index below the row count of its batch. + */ +public final class ArrowProperties { + private static final String UNSAFE_MEMORY_ACCESS = "arrow.enable_unsafe_memory_access"; + private static final String UNSAFE_MEMORY_ACCESS_ENV = "ARROW_ENABLE_UNSAFE_MEMORY_ACCESS"; + private static final String NULL_CHECK_FOR_GET = "arrow.enable_null_check_for_get"; + private static final String NULL_CHECK_FOR_GET_ENV = "ARROW_ENABLE_NULL_CHECK_FOR_GET"; + + static { + setUnlessGiven(UNSAFE_MEMORY_ACCESS, UNSAFE_MEMORY_ACCESS_ENV, "true"); + setUnlessGiven(NULL_CHECK_FOR_GET, NULL_CHECK_FOR_GET_ENV, "false"); + } + + private ArrowProperties() {} + + /** Applies the settings, once per JVM. The class initializer does the work, so a repeated call costs nothing. */ + public static void configure() {} + + private static void setUnlessGiven(String property, String environment, String value) { + if (System.getProperty(property) == null && System.getenv(environment) == null) { + System.setProperty(property, value); + } + } +} diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/VortexSparkSession.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/VortexSparkSession.java index 773a25b7a82..907d895b2cd 100644 --- a/java/vortex-spark/common/src/main/java/dev/vortex/spark/VortexSparkSession.java +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/VortexSparkSession.java @@ -32,6 +32,11 @@ * referenced for the JVM's lifetime. */ public final class VortexSparkSession { + static { + // Executors load this class before any Vortex batch, so Arrow sees the settings before it reads them. + ArrowProperties.configure(); + } + /** Options key used to select a {@link VortexSessionProvider} by class name. */ public static final String PROVIDER_OPTION = "vortex.session.provider"; diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/io/VortexIo.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/io/VortexIo.java index 5bc35a81808..7655c9e5f88 100644 --- a/java/vortex-spark/common/src/main/java/dev/vortex/spark/io/VortexIo.java +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/io/VortexIo.java @@ -9,6 +9,10 @@ import java.util.Locale; import java.util.Objects; import org.apache.hadoop.conf.Configuration; +import org.apache.spark.SparkContext; +import org.apache.spark.broadcast.Broadcast; +import scala.reflect.ClassTag; +import scala.reflect.ClassTag$; /** * The Hadoop configuration this Spark job reaches storage with, and the read settings that go with it. @@ -17,7 +21,7 @@ * providers as Spark's file index and commit protocol. Reads open a {@link HadoopReadable}; writes go through * {@link HadoopWritable} on the task path Spark's commit protocol assigns. * - *

Built on the driver and shipped to executors, so the configuration travels with it. + *

Built on the driver and shipped to executors, so the configuration travels with it, or is broadcast to them. */ public final class VortexIo implements Serializable { private static final long serialVersionUID = 1L; @@ -30,23 +34,42 @@ public final class VortexIo implements Serializable { private static final int DEFAULT_READ_CONCURRENCY = 0; + /** The configuration this instance carries itself, or null when it reads {@link #broadcastConf}. */ private final SerializableHadoopConf conf; + + /** The configuration broadcast to every executor once, or null when this instance carries {@link #conf}. */ + private final Broadcast broadcastConf; + private final int readConcurrency; /** Captures {@code hadoopConf} and the read settings found in the format options. */ public static VortexIo create(VortexOptions options, Configuration hadoopConf) { Objects.requireNonNull(options, "options"); Objects.requireNonNull(hadoopConf, "hadoopConf"); - return new VortexIo(new SerializableHadoopConf(hadoopConf), parseReadConcurrency(options)); + return new VortexIo(new SerializableHadoopConf(hadoopConf), null, parseReadConcurrency(options)); + } + + /** + * Like {@link #create}, but broadcasts {@code hadoopConf} through {@code context}. Each task that deserializes the + * result then shares the executor's copy, instead of reading the whole configuration back from its own bytes. + */ + public static VortexIo broadcast(VortexOptions options, Configuration hadoopConf, SparkContext context) { + Objects.requireNonNull(options, "options"); + Objects.requireNonNull(hadoopConf, "hadoopConf"); + ClassTag tag = ClassTag$.MODULE$.apply(SerializableHadoopConf.class); + return new VortexIo( + null, context.broadcast(new SerializableHadoopConf(hadoopConf), tag), parseReadConcurrency(options)); } /** Hadoop I/O over a default configuration, for callers that have no Spark session to draw one from. */ public static VortexIo defaults() { - return new VortexIo(new SerializableHadoopConf(new Configuration()), DEFAULT_READ_CONCURRENCY); + return new VortexIo(new SerializableHadoopConf(new Configuration()), null, DEFAULT_READ_CONCURRENCY); } - private VortexIo(SerializableHadoopConf conf, int readConcurrency) { + private VortexIo( + SerializableHadoopConf conf, Broadcast broadcastConf, int readConcurrency) { this.conf = conf; + this.broadcastConf = broadcastConf; this.readConcurrency = readConcurrency; } @@ -60,7 +83,7 @@ private static int parseReadConcurrency(VortexOptions options) { } public Configuration hadoopConf() { - return conf.value(); + return conf != null ? conf.value() : broadcastConf.value().value(); } /** Bound on concurrent read upcalls per file; zero keeps the native default. */ diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/SparkFilterToVortexExpression.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/SparkFilterToVortexExpression.java index 365890893b8..4efd901bb39 100644 --- a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/SparkFilterToVortexExpression.java +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/SparkFilterToVortexExpression.java @@ -156,24 +156,38 @@ private static Optional convertIn(In in, StructType dataSchema) { if (column.isEmpty()) { return Optional.empty(); } - List comparisons = new ArrayList<>(); + DataType dataType = column.get().dataType(); + Expression field = column.get().expression(); + + List elements = new ArrayList<>(in.values().length); + boolean holdsNull = false; for (Object value : in.values()) { if (value == null) { + holdsNull = true; continue; } - Optional literal = literal(value, column.get().dataType()); + Optional literal = literal(value, dataType); if (literal.isEmpty()) { return Optional.empty(); } - comparisons.add(Expression.binary(BinaryOp.EQ, column.get().expression(), literal.get())); + elements.add(literal.get()); } - if (comparisons.isEmpty()) { - return Optional.of(Expression.literal(false)); + + if (elements.size() == 1 && !holdsNull) { + return Optional.of(Expression.binary(BinaryOp.EQ, field, elements.get(0))); + } + + // A null element keeps SQL's three-valued IN, so a non-match beneath NOT stays null rather than true. The + // native set answers a list of any length in one pass, where an OR of equalities grows with the list. + Optional elementType = literal(null, dataType); + if (elementType.isEmpty()) { + return Optional.empty(); } - if (comparisons.size() == 1) { - return Optional.of(comparisons.get(0)); + if (holdsNull) { + elements.add(elementType.get()); } - return Optional.of(Expression.or(comparisons.toArray(new Expression[0]))); + return Optional.of(Expression.in( + field, Expression.literalList(elementType.get(), holdsNull, elements.toArray(new Expression[0])))); } private static Optional stringMatch( diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexAggregateReaderFactory.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexAggregateReaderFactory.java index f7335a283dc..d7adea30698 100644 --- a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexAggregateReaderFactory.java +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexAggregateReaderFactory.java @@ -3,6 +3,7 @@ package dev.vortex.spark.read; +import dev.vortex.spark.ArrowProperties; import dev.vortex.spark.VortexOptions; import dev.vortex.spark.io.VortexFile; import dev.vortex.spark.io.VortexIo; @@ -30,6 +31,11 @@ *

A listed file without {@link VortexFile#EXTENSION} is not part of the dataset and produces no row. */ public final class VortexAggregateReaderFactory extends FilePartitionReaderFactory implements Serializable { + static { + // Executors load this class before any Vortex batch, so Arrow sees the settings before it reads them. + ArrowProperties.configure(); + } + private static final long serialVersionUID = 1L; private final FileSourceOptions fileOptions; diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexArrowColumnVector.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexArrowColumnVector.java index 887b0f81aa3..9d141249bd6 100644 --- a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexArrowColumnVector.java +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexArrowColumnVector.java @@ -5,13 +5,12 @@ package dev.vortex.spark.read; import com.jakewharton.nopen.annotation.Open; +import dev.vortex.relocated.org.apache.arrow.memory.ArrowBuf; import dev.vortex.relocated.org.apache.arrow.vector.*; import dev.vortex.relocated.org.apache.arrow.vector.complex.ListVector; import dev.vortex.relocated.org.apache.arrow.vector.complex.ListViewVector; import dev.vortex.relocated.org.apache.arrow.vector.complex.MapVector; import dev.vortex.relocated.org.apache.arrow.vector.complex.StructVector; -import dev.vortex.relocated.org.apache.arrow.vector.holders.NullableLargeVarCharHolder; -import dev.vortex.relocated.org.apache.arrow.vector.holders.NullableVarCharHolder; import dev.vortex.relocated.org.apache.arrow.vector.types.TimeUnit; import dev.vortex.relocated.org.apache.arrow.vector.types.pojo.ArrowType; import dev.vortex.spark.ArrowUtils; @@ -25,6 +24,7 @@ import org.apache.spark.sql.vectorized.ColumnVector; import org.apache.spark.sql.vectorized.ColumnarArray; import org.apache.spark.sql.vectorized.ColumnarMap; +import org.apache.spark.unsafe.Platform; import org.apache.spark.unsafe.types.UTF8String; /** @@ -390,19 +390,58 @@ void initAccessor(ValueVector vector) { } abstract static class ArrowVectorAccessor { + /** Marks a vector whose validity {@link #isNullAt} asks the vector for, as it is not a plain bitmap. */ + private static final long VALIDITY_FROM_VECTOR = -1; final ValueVector vector; + private final int nullCount; + + /** + * The address of the vector's validity bitmap, 0 when every value is valid, or {@link #VALIDITY_FROM_VECTOR}. + * + *

Spark reads every Vortex column as nullable, so generated code checks each value for null. Reading the + * bit directly, or nothing at all, keeps that check off Arrow's interface calls. + */ + private final long validityAddress; + ArrowVectorAccessor(ValueVector vector) { this.vector = vector; + this.nullCount = vector.getNullCount(); + this.validityAddress = validityAddress(vector, nullCount); + } + + private static long validityAddress(ValueVector vector, int nullCount) { + if (nullCount == 0) { + return 0; + } + if (vector instanceof BaseFixedWidthVector + || vector instanceof BaseVariableWidthVector + || vector instanceof BaseLargeVariableWidthVector + || vector instanceof BaseVariableWidthViewVector) { + return vector.getValidityBuffer().memoryAddress(); + } + return VALIDITY_FROM_VECTOR; } final boolean isNullAt(int rowId) { - return vector.isNull(rowId); + long address = validityAddress; + if (address == 0) { + return false; + } + if (address == VALIDITY_FROM_VECTOR) { + return vector.isNull(rowId); + } + return (Platform.getByte(null, address + (rowId >>> 3)) & (1 << (rowId & 7))) == 0; } final int getNullCount() { - return vector.getNullCount(); + return nullCount; + } + + /** The address of a fixed-width vector's values. */ + static long valuesAddress(BaseFixedWidthVector vector) { + return vector.getDataBuffer().memoryAddress(); } boolean getBoolean(int rowId) { @@ -457,112 +496,112 @@ ColumnarMap getMap(int rowId) { @Open static class BooleanAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final BitVector accessor; + private final long values; BooleanAccessor(BitVector vector) { super(vector); - this.accessor = vector; + this.values = valuesAddress(vector); } @Override final boolean getBoolean(int rowId) { - return accessor.get(rowId) == 1; + return (Platform.getByte(null, values + (rowId >>> 3)) & (1 << (rowId & 7))) != 0; } } @Open static class ByteAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final TinyIntVector accessor; + private final long values; ByteAccessor(TinyIntVector vector) { super(vector); - this.accessor = vector; + this.values = valuesAddress(vector); } @Override final byte getByte(int rowId) { - return accessor.get(rowId); + return Platform.getByte(null, values + rowId); } } @Open static class ShortAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final SmallIntVector accessor; + private final long values; ShortAccessor(SmallIntVector vector) { super(vector); - this.accessor = vector; + this.values = valuesAddress(vector); } @Override final short getShort(int rowId) { - return accessor.get(rowId); + return Platform.getShort(null, values + ((long) rowId << 1)); } } @Open static class IntAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final IntVector accessor; + private final long values; IntAccessor(IntVector vector) { super(vector); - this.accessor = vector; + this.values = valuesAddress(vector); } @Override final int getInt(int rowId) { - return accessor.get(rowId); + return Platform.getInt(null, values + ((long) rowId << 2)); } } @Open static class LongAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final BigIntVector accessor; + private final long values; LongAccessor(BigIntVector vector) { super(vector); - this.accessor = vector; + this.values = valuesAddress(vector); } @Override final long getLong(int rowId) { - return accessor.get(rowId); + return Platform.getLong(null, values + ((long) rowId << 3)); } } @Open static class FloatAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final Float4Vector accessor; + private final long values; FloatAccessor(Float4Vector vector) { super(vector); - this.accessor = vector; + this.values = valuesAddress(vector); } @Override final float getFloat(int rowId) { - return accessor.get(rowId); + return Platform.getFloat(null, values + ((long) rowId << 2)); } } @Open static class DoubleAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final Float8Vector accessor; + private final long values; DoubleAccessor(Float8Vector vector) { super(vector); - this.accessor = vector; + this.values = valuesAddress(vector); } @Override final double getDouble(int rowId) { - return accessor.get(rowId); + return Platform.getDouble(null, values + ((long) rowId << 3)); } } @@ -584,6 +623,8 @@ final Decimal getDecimal(int rowId, int precision, int scale) { } static final class SmallDecimalAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { + private static final long[] LONG_POWERS_OF_TEN = longPowersOfTen(); + private final long values; private final DecimalVector accessor; private final int lowWordOffset; @@ -592,20 +633,52 @@ static final class SmallDecimalAccessor extends VortexArrowColumnVector.ArrowVec SmallDecimalAccessor(DecimalVector vector, ByteOrder byteOrder) { super(vector); this.accessor = vector; + this.values = valuesAddress(vector); // Arrow Java stores Decimal128 in native byte order, including the order of its two words. this.lowWordOffset = byteOrder == ByteOrder.LITTLE_ENDIAN ? 0 : Long.BYTES; this.highWordOffset = byteOrder == ByteOrder.LITTLE_ENDIAN ? Long.BYTES : 0; } + private static long[] longPowersOfTen() { + long[] powers = new long[Decimal.MAX_LONG_DIGITS() + 1]; + powers[0] = 1; + for (int i = 1; i < powers.length; i++) { + powers[i] = powers[i - 1] * 10; + } + return powers; + } + + /** + * Whether every value of the source type is a value of {@code DECIMAL(precision, scale)} with the same unscaled + * value, which then fits a long. + */ + private boolean fitsRequestedType(int precision, int scale) { + return scale >= 0 + && scale == accessor.getScale() + && accessor.getPrecision() <= precision + && precision <= Decimal.MAX_LONG_DIGITS(); + } + + /** Whether {@code unscaled} has at most the source precision's digits, as every valid value does. */ + private boolean fitsSourcePrecision(long unscaled) { + long bound = LONG_POWERS_OF_TEN[accessor.getPrecision()]; + return unscaled < bound && unscaled > -bound; + } + @Override final Decimal getDecimal(int rowId, int precision, int scale) { if (isNullAt(rowId)) { return null; } - long offset = (long) rowId * DecimalVector.TYPE_WIDTH; - long unscaled = accessor.getDataBuffer().getLong(offset + lowWordOffset); - long high = accessor.getDataBuffer().getLong(offset + highWordOffset); + long value = values + (long) rowId * DecimalVector.TYPE_WIDTH; + long unscaled = Platform.getLong(null, value + lowWordOffset); + long high = Platform.getLong(null, value + highWordOffset); if (high == (unscaled >> 63)) { + if (fitsRequestedType(precision, scale) && fitsSourcePrecision(unscaled)) { + // The value is a value of the requested type as it is. Spark's own vectorized readers return + // such decimals as a long, without a BigDecimal per value. + return Decimal.createUnsafe(unscaled, precision, scale); + } // Decode with the source scale; Spark performs the requested rescaling and overflow checks. // Keep the expanded Decimal representation for Spark's checked integer casts. return Decimal.apply(BigDecimal.valueOf(unscaled, accessor.getScale()), precision, scale); @@ -615,72 +688,85 @@ final Decimal getDecimal(int rowId, int precision, int scale) { } } + /** + * Reads strings straight from the vector's buffers, whose addresses it keeps. The column checks for null before it + * asks for a value, so the accessor does not check again. + */ @Open static class StringAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final VarCharVector accessor; - private final NullableVarCharHolder stringResult = new NullableVarCharHolder(); + private final long offsets; + private final long data; StringAccessor(VarCharVector vector) { super(vector); - this.accessor = vector; + this.offsets = vector.getOffsetBuffer().memoryAddress(); + this.data = vector.getDataBuffer().memoryAddress(); } @Override final UTF8String getUTF8String(int rowId) { - accessor.get(rowId, stringResult); - if (stringResult.isSet == 0) { - return null; - } else { - return UTF8String.fromAddress( - null, - stringResult.buffer.memoryAddress() + stringResult.start, - stringResult.end - stringResult.start); - } + long offset = offsets + ((long) rowId << 2); + int start = Platform.getInt(null, offset); + int end = Platform.getInt(null, offset + Integer.BYTES); + return UTF8String.fromAddress(null, data + start, end - start); } } + /** Reads large strings like {@link StringAccessor}, through 64-bit offsets. */ @Open static class LargeStringAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final LargeVarCharVector accessor; - private final NullableLargeVarCharHolder stringResult = new NullableLargeVarCharHolder(); + private final long offsets; + private final long data; LargeStringAccessor(LargeVarCharVector vector) { super(vector); - this.accessor = vector; + this.offsets = vector.getOffsetBuffer().memoryAddress(); + this.data = vector.getDataBuffer().memoryAddress(); } @Override final UTF8String getUTF8String(int rowId) { - accessor.get(rowId, stringResult); - if (stringResult.isSet == 0) { - return null; - } else { - return UTF8String.fromAddress( - null, - stringResult.buffer.memoryAddress() + stringResult.start, - // A single string cannot be larger than the max integer size, so the conversion is safe - (int) (stringResult.end - stringResult.start)); - } + long offset = offsets + ((long) rowId << 3); + long start = Platform.getLong(null, offset); + long end = Platform.getLong(null, offset + Long.BYTES); + // A single string cannot be larger than the max integer size, so the conversion is safe. + return UTF8String.fromAddress(null, data + start, (int) (end - start)); } } @Open static class StringViewAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final ViewVarCharVector accessor; + private final long views; + + /** The address of each data buffer, by the index a view gives. */ + private final long[] data; StringViewAccessor(ViewVarCharVector vector) { super(vector); - this.accessor = vector; + this.views = vector.getDataBuffer().memoryAddress(); + this.data = vector.getDataBuffers().stream() + .mapToLong(ArrowBuf::memoryAddress) + .toArray(); } + /** + * Aliases the value's bytes, like {@link StringAccessor}. A view holds its length in its first four bytes, and + * then either the value itself when it is short, or a prefix, the index of a data buffer, and an offset into it. + */ @Override final UTF8String getUTF8String(int rowId) { - // View values may be inlined in the view buffer rather than contiguous in a data - // buffer, so copy out rather than aliasing vector memory. - return UTF8String.fromBytes(accessor.get(rowId)); + long view = views + (long) rowId * BaseVariableWidthViewVector.ELEMENT_SIZE; + int length = Platform.getInt(null, view); + if (length <= BaseVariableWidthViewVector.INLINE_SIZE) { + return UTF8String.fromAddress(null, view + Integer.BYTES, length); + } + + long buffer = data[Platform.getInt(null, view + 2L * Integer.BYTES)]; + long offset = Integer.toUnsignedLong(Platform.getInt(null, view + 3L * Integer.BYTES)); + return UTF8String.fromAddress(null, buffer + offset, length); } } @@ -735,16 +821,16 @@ final byte[] getBinary(int rowId) { @Open static class DateAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final DateDayVector accessor; + private final long values; DateAccessor(DateDayVector vector) { super(vector); - this.accessor = vector; + this.values = valuesAddress(vector); } @Override final int getInt(int rowId) { - return accessor.get(rowId); + return Platform.getInt(null, values + ((long) rowId << 2)); } } @@ -757,18 +843,18 @@ final int getInt(int rowId) { @Open static class TimestampAccessor extends VortexArrowColumnVector.ArrowVectorAccessor { - private final TimeStampVector accessor; + private final long values; private final TimeUnit unit; TimestampAccessor(TimeStampVector vector) { super(vector); - this.accessor = vector; + this.values = valuesAddress(vector); this.unit = ((ArrowType.Timestamp) vector.getField().getType()).getUnit(); } @Override final long getLong(int rowId) { - long value = accessor.get(rowId); + long value = Platform.getLong(null, values + ((long) rowId << 3)); return switch (unit) { case SECOND -> Math.multiplyExact(value, 1_000_000L); case MILLISECOND -> Math.multiplyExact(value, 1_000L); diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexPartitionReaderFactory.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexPartitionReaderFactory.java index 8314d75790b..13f8ee06922 100644 --- a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexPartitionReaderFactory.java +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexPartitionReaderFactory.java @@ -3,6 +3,7 @@ package dev.vortex.spark.read; +import dev.vortex.spark.ArrowProperties; import dev.vortex.spark.VortexOptions; import dev.vortex.spark.io.VortexFile; import dev.vortex.spark.io.VortexIo; @@ -24,6 +25,11 @@ *

A listed file without {@link VortexFile#EXTENSION} is not part of the dataset and reads as empty. */ public final class VortexPartitionReaderFactory extends FilePartitionReaderFactory implements Serializable { + static { + // Executors load this class before any Vortex batch, so Arrow sees the settings before it reads them. + ArrowProperties.configure(); + } + private static final long serialVersionUID = 1L; private final FileSourceOptions fileOptions; diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/write/VortexOutputWriterFactory.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/write/VortexOutputWriterFactory.java index 705893c594f..fb5ee18669b 100644 --- a/java/vortex-spark/common/src/main/java/dev/vortex/spark/write/VortexOutputWriterFactory.java +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/write/VortexOutputWriterFactory.java @@ -3,6 +3,7 @@ package dev.vortex.spark.write; +import dev.vortex.spark.ArrowProperties; import dev.vortex.spark.VortexOptions; import dev.vortex.spark.io.HadoopWritable; import org.apache.hadoop.mapreduce.TaskAttemptContext; @@ -12,6 +13,11 @@ /** Creates Vortex output writers at paths assigned by Spark's file commit protocol. */ public final class VortexOutputWriterFactory extends OutputWriterFactory { + static { + // Executors load this class before any Vortex batch, so Arrow sees the settings before it reads them. + ArrowProperties.configure(); + } + private static final long serialVersionUID = 1L; private final StructType schema; diff --git a/java/vortex-spark/common/src/main/scala/dev/vortex/spark/VortexFileFormat.scala b/java/vortex-spark/common/src/main/scala/dev/vortex/spark/VortexFileFormat.scala index 7039ada4538..0ca50c839ec 100644 --- a/java/vortex-spark/common/src/main/scala/dev/vortex/spark/VortexFileFormat.scala +++ b/java/vortex-spark/common/src/main/scala/dev/vortex/spark/VortexFileFormat.scala @@ -101,7 +101,8 @@ final class VortexFileFormat hadoopConf: Configuration ): PartitionedFile => Iterator[InternalRow] = { val optionMap = VortexOptions.of(options.asJava) - val io = VortexIo.create(optionMap, hadoopConf) + // Broadcast once, as Spark's own file formats do, rather than shipped with every task. + val io = VortexIo.broadcast(optionMap, hadoopConf, spark.sparkContext) val caseSensitive = spark.sessionState.conf.caseSensitiveAnalysis skippingNonVortexFiles { (file: PartitionedFile) => val reader = new VortexPartitionReader( @@ -173,7 +174,8 @@ final class VortexFileFormat } val optionMap = VortexOptions.of(options.asJava) - val io = VortexIo.create(optionMap, hadoopConf) + // Broadcast once, as Spark's own file formats do, rather than shipped with every task. + val io = VortexIo.broadcast(optionMap, hadoopConf, spark.sparkContext) val caseSensitive = spark.sessionState.conf.caseSensitiveAnalysis skippingNonVortexFiles { (file: PartitionedFile) => val reader = new VortexPartitionReader( diff --git a/java/vortex-spark/common/src/main/scala/dev/vortex/spark/read/VortexScan.scala b/java/vortex-spark/common/src/main/scala/dev/vortex/spark/read/VortexScan.scala index bcb664a73c3..aa62b836686 100644 --- a/java/vortex-spark/common/src/main/scala/dev/vortex/spark/read/VortexScan.scala +++ b/java/vortex-spark/common/src/main/scala/dev/vortex/spark/read/VortexScan.scala @@ -49,6 +49,12 @@ final class VortexScan( private val hadoopConf = sparkSession.sessionState.newHadoopConfWithOptions(caseSensitiveOptions) private val io = VortexIo.create(options, hadoopConf) + + /** The I/O the readers use, whose configuration is broadcast once rather than + * shipped with every task. + */ + private lazy val executorIo = + VortexIo.broadcast(options, hadoopConf, sparkSession.sparkContext) private val caseSensitive = sparkSession.sessionState.conf.caseSensitiveAnalysis @@ -63,7 +69,7 @@ final class VortexScan( case Some(aggregation) => new VortexAggregateReaderFactory( fileOptions, - io, + executorIo, options, readDataSchema, readPartitionSchema, @@ -72,7 +78,7 @@ final class VortexScan( case None => new VortexPartitionReaderFactory( fileOptions, - io, + executorIo, options, dataSchema, readDataSchema, diff --git a/java/vortex-spark/common/src/test/java/dev/vortex/spark/VortexFilterPushdownTest.java b/java/vortex-spark/common/src/test/java/dev/vortex/spark/VortexFilterPushdownTest.java index 3d4324ee89e..c4bf7150156 100644 --- a/java/vortex-spark/common/src/test/java/dev/vortex/spark/VortexFilterPushdownTest.java +++ b/java/vortex-spark/common/src/test/java/dev/vortex/spark/VortexFilterPushdownTest.java @@ -142,6 +142,15 @@ public void testFilterPushdownCorrectness() throws IOException { List.of(1, 3), idsOf(readDf.filter(readDf.col("name").isin("alpha", "gamma")).orderBy("id"))); + // NOT IN with a null element is never true in SQL, so the native set must not admit a non-match. + assertEquals( + List.of(), + idsOf(readDf.filter(functions.not(readDf.col("id").isin(1, null))) + .orderBy("id"))); + assertEquals( + List.of(2, 3, 4, 5), + idsOf(readDf.filter(functions.not(readDf.col("id").isin(1))).orderBy("id"))); + assertEquals( List.of(4), idsOf(readDf.filter(readDf.col("amount").isNull()).orderBy("id"))); diff --git a/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/SparkFilterToVortexExpressionTest.java b/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/SparkFilterToVortexExpressionTest.java index 9236697728c..028c2c86f5a 100644 --- a/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/SparkFilterToVortexExpressionTest.java +++ b/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/SparkFilterToVortexExpressionTest.java @@ -62,10 +62,11 @@ void convertsNullSafeEqualityAndNegation() { @Test void convertsInListsHoldingNulls() { - // SQL never matches a null through IN, so the null values drop out of the disjunction. + // The nulls stay in the native set, so a non-match is null under NOT as well, as in SQL. assertPushable(new In("id", new Object[] {1, null})); - // Nothing but nulls can never match, so the filter becomes a constant false. + assertPushable(new Not(new In("id", new Object[] {1, null}))); assertPushable(new In("id", new Object[] {null})); + assertPushable(new In("name", new Object[] {"a", "b", "c"})); } @Test diff --git a/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/VortexDecimalAccessorTest.java b/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/VortexDecimalAccessorTest.java index 3014c7b565e..897d7ea1bed 100644 --- a/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/VortexDecimalAccessorTest.java +++ b/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/VortexDecimalAccessorTest.java @@ -170,7 +170,8 @@ void outOfPrecisionBufferValuesRetainReferenceResultsAndErrors() { assertMatchesReference(vector, column, row, 38, 0); } vector.setNull(0); - assertNull(column.getDecimal(0, 18, 0)); + // A column vector describes its batch as it was when wrapped, so wrap the changed batch again. + assertNull(new VortexArrowColumnVector(vector).getDecimal(0, 18, 0)); } } @@ -251,7 +252,10 @@ void checkedIntegralConversionsRetainResultsAndOverflow() { vector.set(0, input); vector.setValueCount(1); VortexArrowColumnVector column = new VortexArrowColumnVector(vector); - Decimal expected = Decimal.apply(vector.getObject(0), precision, scale); + // Spark's own vectorized readers return a decimal of at most 18 digits as its unscaled long, + // and integral conversions of that representation truncate rather than round through a double. + Decimal expected = + Decimal.createUnsafe(input.unscaledValue().longValueExact(), precision, scale); Decimal actual = column.getDecimal(0, precision, scale); assertConversion(expected::roundToByte, actual::roundToByte); assertConversion(expected::roundToShort, actual::roundToShort); diff --git a/vortex-jni/src/writer.rs b/vortex-jni/src/writer.rs index a8c14a5fc67..917ca9bb5a7 100644 --- a/vortex-jni/src/writer.rs +++ b/vortex-jni/src/writer.rs @@ -214,8 +214,10 @@ pub extern "system" fn Java_dev_vortex_jni_NativeWriter_create( } let session = unsafe { session_ref(session_ptr) }; - let ffi_schema = unsafe { &*(arrow_schema_addr as *const FFI_ArrowSchema) }; - let arrow_schema = Arc::new(Schema::try_from(ffi_schema)?); + // Owned, so dropping it releases the exporter's copy as the C data interface requires. + let ffi_schema = + unsafe { FFI_ArrowSchema::from_raw(arrow_schema_addr as *mut FFI_ArrowSchema) }; + let arrow_schema = Arc::new(Schema::try_from(&ffi_schema)?); let write_schema = session.arrow().from_arrow_schema(arrow_schema.as_ref())?; let file_path: String = uri.try_to_string(env)?; @@ -301,8 +303,10 @@ pub extern "system" fn Java_dev_vortex_jni_NativeWriter_createStream( } let session = unsafe { session_ref(session_ptr) }; - let ffi_schema = unsafe { &*(arrow_schema_addr as *const FFI_ArrowSchema) }; - let arrow_schema = Arc::new(Schema::try_from(ffi_schema)?); + // Owned, so dropping it releases the exporter's copy as the C data interface requires. + let ffi_schema = + unsafe { FFI_ArrowSchema::from_raw(arrow_schema_addr as *mut FFI_ArrowSchema) }; + let arrow_schema = Arc::new(Schema::try_from(&ffi_schema)?); let write_schema = session.arrow().from_arrow_schema(arrow_schema.as_ref())?; let metadata = extract_metadata(env, &metadata)?; @@ -354,9 +358,12 @@ pub extern "system" fn Java_dev_vortex_jni_NativeWriter_writeBatch( let ffi_array = unsafe { FFI_ArrowArray::from_raw(arrow_array_addr as *mut FFI_ArrowArray) }; - let ffi_schema = unsafe { &*(arrow_schema_addr as *const FFI_ArrowSchema) }; + // Take the schema as well as the array. As the consumer, native code must release both, and dropping the + // owned schema does that; borrowing it would leak the exporter's copy on every batch. + let ffi_schema = + unsafe { FFI_ArrowSchema::from_raw(arrow_schema_addr as *mut FFI_ArrowSchema) }; - let array_data = unsafe { arrow_array::ffi::from_ffi(ffi_array, ffi_schema) } + let array_data = unsafe { arrow_array::ffi::from_ffi(ffi_array, &ffi_schema) } .map_err(|e| JNIError::Vortex(vortex_err!("failed to import Arrow FFI data: {e}")))?; let batch = RecordBatch::from(StructArray::from(array_data)); diff --git a/vortex-layout/src/layouts/file_stats.rs b/vortex-layout/src/layouts/file_stats.rs index ddde8c163b8..46d4e5ab7c1 100644 --- a/vortex-layout/src/layouts/file_stats.rs +++ b/vortex-layout/src/layouts/file_stats.rs @@ -341,8 +341,23 @@ impl StatsAccumulator { } } +/// Whether `dtype` is, or holds anywhere within it, a variant, which no builder can hold. +pub(crate) fn contains_variant(dtype: &DType) -> bool { + match dtype { + DType::Variant(_) => true, + DType::Struct(fields, _) => fields.fields().any(|field| contains_variant(&field)), + DType::List(element, _) | DType::FixedSizeList(element, ..) => contains_variant(element), + DType::Map(map, _) => { + contains_variant(&map.key_dtype()) || contains_variant(&map.value_dtype()) + } + DType::Extension(ext) => contains_variant(ext.storage_dtype()), + _ => false, + } +} + fn supports_file_stats(dtype: &DType) -> bool { - !matches!(dtype, DType::Variant(_)) + // A variant, also one nested in a struct or list, has no builder to hold its statistics. + !contains_variant(dtype) } fn is_varlen_dtype(dtype: &DType) -> bool { @@ -831,6 +846,7 @@ mod tests { use vortex_array::builders::VarBinViewBuilder; use vortex_array::dtype::FieldNames; use vortex_array::dtype::PType; + use vortex_array::dtype::StructFields; use vortex_array::scalar::PValue; use vortex_array::scalar::ScalarValue; use vortex_array::validity::Validity; @@ -1001,6 +1017,21 @@ mod tests { Ok(()) } + #[rstest] + #[case::variant(DType::Variant(Nullability::Nullable))] + #[case::variant_in_struct(DType::Struct( + StructFields::new(["v"].into(), vec![DType::Variant(Nullability::Nullable)]), + Nullability::NonNullable, + ))] + #[case::variant_in_list(DType::List( + Arc::new(DType::Variant(Nullability::Nullable)), + Nullability::Nullable, + ))] + fn variant_columns_record_no_file_statistics(#[case] dtype: DType) { + let acc = StatsAccumulator::new(&dtype, None, &|_| true); + assert!(acc.aggregates.is_empty()); + } + fn i32_dtype() -> DType { DType::Primitive(PType::I32, Nullability::NonNullable) } diff --git a/vortex-layout/src/layouts/zoned/builder.rs b/vortex-layout/src/layouts/zoned/builder.rs index 97290ad731d..a5d3250310e 100644 --- a/vortex-layout/src/layouts/zoned/builder.rs +++ b/vortex-layout/src/layouts/zoned/builder.rs @@ -19,6 +19,7 @@ use vortex_array::validity::Validity; use vortex_error::VortexResult; use vortex_error::vortex_ensure_eq; +use crate::layouts::file_stats::contains_variant; use crate::layouts::zoned::schema::aggregate_state_dtype; /// Accumulates aggregate-function partials for each logical zone. @@ -32,13 +33,16 @@ impl AggregateStatsAccumulator { let builders = aggregate_fns .iter() .filter_map(|aggregate_fn| { - aggregate_state_dtype(dtype, aggregate_fn).map(|partial_dtype| { - AggregateStatsArrayBuilder::new( - aggregate_fn.clone(), - &partial_dtype.as_nullable(), - 1024, - ) - }) + aggregate_state_dtype(dtype, aggregate_fn) + // A variant state has no builder, so a zone map cannot hold it. + .filter(|partial_dtype| !contains_variant(partial_dtype)) + .map(|partial_dtype| { + AggregateStatsArrayBuilder::new( + aggregate_fn.clone(), + &partial_dtype.as_nullable(), + 1024, + ) + }) }) .collect::>(); diff --git a/vortex-layout/src/layouts/zoned/writer.rs b/vortex-layout/src/layouts/zoned/writer.rs index 696b7a412e7..09044047e73 100644 --- a/vortex-layout/src/layouts/zoned/writer.rs +++ b/vortex-layout/src/layouts/zoned/writer.rs @@ -363,6 +363,7 @@ mod tests { use vortex_array::arrays::ChunkedArray; use vortex_array::dtype::Nullability; use vortex_array::dtype::PType; + use vortex_array::dtype::StructFields; use vortex_array::extension::datetime::TimeUnit; use vortex_array::extension::datetime::Timestamp; use vortex_buffer::buffer; @@ -375,8 +376,10 @@ mod tests { use super::*; use crate::layouts::chunked::writer::ChunkedLayoutStrategy; + use crate::layouts::file_stats::contains_variant; use crate::layouts::flat::writer::FlatLayoutStrategy; use crate::layouts::zoned::Zoned; + use crate::layouts::zoned::schema::aggregate_state_dtype; use crate::segments::TestSegments; use crate::sequence::SequenceId; use crate::sequence::SequentialArrayStreamExt; @@ -384,6 +387,22 @@ mod tests { static SESSION: LazyLock = LazyLock::new(VortexSession::empty); + #[rstest] + #[case::variant(DType::Variant(Nullability::Nullable))] + #[case::variant_in_struct(DType::Struct( + StructFields::new(["v"].into(), vec![DType::Variant(Nullability::Nullable)]), + Nullability::NonNullable, + ))] + fn zone_maps_hold_no_variant_state(#[case] dtype: DType) { + let session = SESSION.clone().with::(); + let accumulator = + AggregateStatsAccumulator::new(&dtype, &default_zoned_aggregate_fns(&dtype, &session)); + for aggregate_fn in accumulator.aggregate_fns().iter() { + let state = aggregate_state_dtype(&dtype, aggregate_fn).vortex_expect("kept state"); + assert!(!contains_variant(&state), "{aggregate_fn} keeps a variant"); + } + } + /// Write three zones of primitives through `ctx`, returning the aggregates the zoned /// layout recorded. fn write_zones(ctx: LayoutWriterContext) -> VortexResult> { From c9a3fbe7c2ec0324c1a8221f878df59ca8395553 Mon Sep 17 00:00:00 2001 From: Matthew Katz <87445739+mhk197@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:03:42 -0500 Subject: [PATCH 18/41] Drop null patch elements when sparse lists share their elements (#11182) ## Summary Since #11173, sparse lists with a null or empty fill reuse the patch lists' elements in their output. When the patch lists are already zero-copy to a list, `MakeExact` only trims the ends, so null patch lists keep their elements. Those elements are exported and written. This PR uses `MakeExactWithEmptyNulls` on the null or empty fill path so that null patches are lists of length 0. ## Changes - The shared-elements path rebuilds the patch lists with `MakeExactWithEmptyNulls`. - If null lists hold nothing, the rebuild returns the array as is; otherwise the elements become a lazy filter over the ranges valid lists use. - `empty_fill_reuses_elements` test now uses a null patch with no elements. Signed-off-by: Matt Katz --- encodings/sparse/src/canonical.rs | 18 +++++----- encodings/sparse/src/canonical/lists/tests.rs | 36 +++++++++++++++++-- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/encodings/sparse/src/canonical.rs b/encodings/sparse/src/canonical.rs index 662c15b0d1d..418abea5a51 100644 --- a/encodings/sparse/src/canonical.rs +++ b/encodings/sparse/src/canonical.rs @@ -378,25 +378,25 @@ fn execute_sparse_lists( let indices = indices.reinterpret_cast(indices.ptype().to_unsigned()); let fill_list = fill_value.as_list(); - // Make the patch views contiguous and trim unused elements once. Empty-fill decoding can - // share those elements directly; the nonempty-fill builder below can append contiguous - // patch runs and use the exact element count when choosing an offset width. - let values = resolved - .values() - .clone() - .downcast::() - .rebuild(ListViewRebuildMode::MakeExact, ctx)?; + let values = resolved.values().clone().downcast::(); // Keep the builder for at most one valid list: there is no repeated element gather to // avoid, and constructing run-end offsets would add setup work. let patch_validity = values.listview_validity().execute_mask(values.len(), ctx)?; let use_shared_elements = patch_validity.true_count() > 1; - // Null lists also have no elements, so null and empty fills take the same path. + // Null lists also have no elements, so null and empty fills take the same path. Its output + // shares the patch elements, so drop the ones only null lists hold: exports and element-wise + // functions would otherwise read elements that no valid row shows. if fill_list.is_empty() && use_shared_elements { + let values = values.rebuild(ListViewRebuildMode::MakeExactWithEmptyNulls, ctx)?; return lists::empty_fill(resolved, values, fill_value, len, nullability, ctx); } + // Trim the patch elements to those the views reference, so the element count below is what + // the builder will hold rather than an over-estimate. + let values = values.rebuild(ListViewRebuildMode::MakeExact, ctx)?; + // Each gap between patches is appended as one constant array, whose canonical form points // every view at a single copy of the fill value, so a gap costs the fill value's elements once // however many rows it covers. Bound the number of gaps: there is at most one on either side of diff --git a/encodings/sparse/src/canonical/lists/tests.rs b/encodings/sparse/src/canonical/lists/tests.rs index 87455d095c5..ca961b4d39e 100644 --- a/encodings/sparse/src/canonical/lists/tests.rs +++ b/encodings/sparse/src/canonical/lists/tests.rs @@ -27,10 +27,10 @@ fn empty_fill_reuses_elements(#[values(false, true)] null_fill: bool) -> VortexR crate::initialize(&session); let mut ctx = session.create_execution_ctx(); - let elements = buffer![1i32, 2, 8, 9, 3].into_array(); + let elements = buffer![1i32, 2, 3].into_array(); let lists = ListArray::try_new( elements.clone(), - buffer![0u32, 2, 4, 5].into_array(), + buffer![0u32, 2, 2, 3].into_array(), Validity::from_iter([true, false, true]), &mut ctx, )?; @@ -69,6 +69,38 @@ fn empty_fill_reuses_elements(#[values(false, true)] null_fill: bool) -> VortexR Ok(()) } +#[test] +fn empty_fill_drops_null_patch_elements() -> VortexResult<()> { + let session = VortexSession::empty(); + crate::initialize(&session); + let mut ctx = session.create_execution_ctx(); + + // The null patch still holds [8, 9], as a masked list does. + let lists = ListArray::try_new( + buffer![1i32, 2, 8, 9, 3].into_array(), + buffer![0u32, 2, 4, 5].into_array(), + Validity::from_iter([true, false, true]), + &mut ctx, + )?; + let fill = Scalar::null(lists.dtype().clone()); + let actual = Sparse::try_new( + buffer![1u32, 4, 7].into_array(), + lists.into_array(), + 10, + fill, + &mut ctx, + )? + .into_array() + .execute_until::(&mut ctx)?; + + assert_arrays_eq!( + actual.as_::().elements().clone(), + buffer![1i32, 2, 3].into_array(), + &mut ctx + ); + Ok(()) +} + #[test] fn terminal_offset_wider_than_view_offsets() -> VortexResult<()> { let session = VortexSession::empty(); From 0e8442b7f42fedc0a981d9a3a8493a4043175e3d Mon Sep 17 00:00:00 2001 From: Will Manning Date: Wed, 4 Nov 2026 10:34:43 -0500 Subject: [PATCH 19/41] ci(bench): revalidate the benchmarks website once per develop push (#11192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: each benchmark job sent `POST /api/revalidate` and a warm pass of about 26 requests to bench.vortex.dev. During develop runs, this occurred about 15 to 60 times an hour. After: a reusable `refresh-bench-site.yml` workflow sends one revalidate and one warm pass. `develop-bench.yml` and `nightly-bench.yml` call it once per run, after all ingests, with `always()`. `commit-metadata.yml` calls it after its commit row. Thus a develop push gets two refreshes and a nightly run gets one. A failed revalidate is tried again three times. Ingestion stays in each job. The request shape agrees with `bench/CONTRACT.md`. Checks: `yamllint --strict`, ruff and pytest pass. A local test server showed the correct requests. `typos` was not installed. I read the workflow graph; no workflow run was started. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Will Manning --- .github/workflows/ci.yml | 1 + .github/workflows/commit-metadata.yml | 12 +- .github/workflows/develop-bench.yml | 12 +- .github/workflows/nightly-bench.yml | 9 ++ .github/workflows/refresh-bench-site.yml | 29 +++++ .github/workflows/sql-bench-matrix.yml | 2 - scripts/post-ingest.py | 70 +----------- scripts/refresh-bench-site.py | 136 +++++++++++++++++++++++ scripts/tests/test_refresh_bench_site.py | 85 ++++++++++++++ 9 files changed, 282 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/refresh-bench-site.yml create mode 100755 scripts/refresh-bench-site.py create mode 100644 scripts/tests/test_refresh_bench_site.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3844ff0ee1..b13556ebb71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,6 +105,7 @@ jobs: --with tabulate --with xxhash \ pytest scripts/tests/test_measurement_id.py \ scripts/tests/test_benchmark_reporting.py scripts/tests/test_post_ingest.py \ + scripts/tests/test_refresh_bench_site.py \ scripts/tests/test_codspeed_shards.py python-test: diff --git a/.github/workflows/commit-metadata.yml b/.github/workflows/commit-metadata.yml index a412b22d6fc..dfc74a3c158 100644 --- a/.github/workflows/commit-metadata.yml +++ b/.github/workflows/commit-metadata.yml @@ -47,8 +47,6 @@ jobs: RDS_BENCH_INSTANCE_ENDPOINT: ${{ vars.RDS_BENCH_INSTANCE_ENDPOINT }} RDS_BENCH_DB_NAME: ${{ vars.RDS_BENCH_DB_NAME }} AWS_REGION: ${{ vars.RDS_BENCH_REGION }} - BENCH_SITE_BASE_URL: ${{ vars.BENCH_SITE_BASE_URL }} - BENCH_REVALIDATE_TOKEN: ${{ secrets.BENCH_REVALIDATE_TOKEN }} run: | set -Eeuo pipefail echo -n > empty.jsonl @@ -61,3 +59,13 @@ jobs: --commit-sha "${{ github.sha }}" \ --branch "${{ github.ref_name }}" \ --region "${AWS_REGION}" + + # Tell the benchmarks website that the new commit exists. develop-bench.yml refreshes again + # after its benchmark results are ingested, but it does not wait for this workflow, so a push + # whose benchmark ingests all fail could otherwise leave this commit hidden by the cache. + refresh-site: + needs: commit-metadata + if: vars.GH_BENCH_INGEST_ROLE_ARN != '' + uses: ./.github/workflows/refresh-bench-site.yml + secrets: + BENCH_REVALIDATE_TOKEN: ${{ secrets.BENCH_REVALIDATE_TOKEN }} diff --git a/.github/workflows/develop-bench.yml b/.github/workflows/develop-bench.yml index e2b0a26f4e6..11ab45fae97 100644 --- a/.github/workflows/develop-bench.yml +++ b/.github/workflows/develop-bench.yml @@ -236,8 +236,6 @@ jobs: RDS_BENCH_INSTANCE_ENDPOINT: ${{ vars.RDS_BENCH_INSTANCE_ENDPOINT }} RDS_BENCH_DB_NAME: ${{ vars.RDS_BENCH_DB_NAME }} AWS_REGION: ${{ vars.RDS_BENCH_REGION }} - BENCH_SITE_BASE_URL: ${{ vars.BENCH_SITE_BASE_URL }} - BENCH_REVALIDATE_TOKEN: ${{ secrets.BENCH_REVALIDATE_TOKEN }} run: | set -Eeuo pipefail curl -fsSL https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \ @@ -264,3 +262,13 @@ jobs: with: mode: "develop" matrix_preset: "develop" + + # Tell the benchmarks website that new results exist, once per push, after every benchmark job + # of the push has ingested its results. `always()` runs it even when a benchmark job failed, so + # the results that other jobs ingested still appear. + refresh-site: + needs: [bench, sql] + if: ${{ always() }} + uses: ./.github/workflows/refresh-bench-site.yml + secrets: + BENCH_REVALIDATE_TOKEN: ${{ secrets.BENCH_REVALIDATE_TOKEN }} diff --git a/.github/workflows/nightly-bench.yml b/.github/workflows/nightly-bench.yml index 879edc0ba45..0946a458b7c 100644 --- a/.github/workflows/nightly-bench.yml +++ b/.github/workflows/nightly-bench.yml @@ -31,3 +31,12 @@ jobs: machine_type: - id: arm64 instance_name: c8gd.metal-24xl + + # Tell the benchmarks website that new results exist, once per run, after every machine type + # has ingested its results. `always()` runs it even when one machine type failed. + refresh-site: + needs: sql + if: ${{ always() }} + uses: ./.github/workflows/refresh-bench-site.yml + secrets: + BENCH_REVALIDATE_TOKEN: ${{ secrets.BENCH_REVALIDATE_TOKEN }} diff --git a/.github/workflows/refresh-bench-site.yml b/.github/workflows/refresh-bench-site.yml new file mode 100644 index 00000000000..d7d841757fa --- /dev/null +++ b/.github/workflows/refresh-bench-site.yml @@ -0,0 +1,29 @@ +# Tells the benchmarks website that new results exist, then warms its default pages. Callers run +# it once per run, after their benchmark ingests finish, instead of once per benchmark job. This +# keeps the website from flushing and refilling its cache dozens of times an hour while +# benchmarks run. The script does nothing when the URL or the token is not set. + +name: Refresh benchmarks website + +on: + workflow_call: + secrets: + BENCH_REVALIDATE_TOKEN: + required: false + +jobs: + refresh: + if: vars.BENCH_SITE_BASE_URL != '' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + sparse-checkout: scripts + - name: Revalidate and warm the benchmarks website + env: + BENCH_SITE_BASE_URL: ${{ vars.BENCH_SITE_BASE_URL }} + BENCH_REVALIDATE_TOKEN: ${{ secrets.BENCH_REVALIDATE_TOKEN }} + run: python3 scripts/refresh-bench-site.py diff --git a/.github/workflows/sql-bench-matrix.yml b/.github/workflows/sql-bench-matrix.yml index f6bc7d791dd..99cb85b1c18 100644 --- a/.github/workflows/sql-bench-matrix.yml +++ b/.github/workflows/sql-bench-matrix.yml @@ -308,8 +308,6 @@ jobs: RDS_BENCH_INSTANCE_ENDPOINT: ${{ vars.RDS_BENCH_INSTANCE_ENDPOINT }} RDS_BENCH_DB_NAME: ${{ vars.RDS_BENCH_DB_NAME }} AWS_REGION: ${{ vars.RDS_BENCH_REGION }} - BENCH_SITE_BASE_URL: ${{ vars.BENCH_SITE_BASE_URL }} - BENCH_REVALIDATE_TOKEN: ${{ secrets.BENCH_REVALIDATE_TOKEN }} run: | set -Eeuo pipefail curl -fsSL https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \ diff --git a/scripts/post-ingest.py b/scripts/post-ingest.py index 8fda2f2784f..d314dd45a1a 100755 --- a/scripts/post-ingest.py +++ b/scripts/post-ingest.py @@ -27,13 +27,10 @@ import argparse import json import math -import os import subprocess import sys import time -import urllib.request from collections.abc import Callable -from concurrent.futures import ThreadPoolExecutor from pathlib import Path from types import ModuleType from typing import TypeVar @@ -95,12 +92,6 @@ def parse_args() -> argparse.Namespace: default=None, help="Run `git show` in this directory (default: current directory).", ) - parser.add_argument( - "--timeout", - type=float, - default=30.0, - help="Site cache refresh timeout in seconds (default: 30).", - ) return parser.parse_args() @@ -939,59 +930,6 @@ def connect_postgres(dsn: str, region: str | None) -> object: return conn -def _http(method: str, url: str, token: str | None, timeout: float) -> bytes: - """Issue one HTTP request and return the body. Raises on any non-2xx or - transport error; callers in `refresh_site_cache` swallow those.""" - headers = {"accept": "application/json"} - if token is not None: - headers["authorization"] = f"Bearer {token}" - req = urllib.request.Request(url, method=method, headers=headers) - with urllib.request.urlopen(req, timeout=timeout) as resp: - return resp.read() - - -def _warm_default_windows(base: str, timeout: float) -> None: - """Best-effort warm pass: prime the freshly invalidated Data Cache for the - landing page and every group's default last-100 bundle, so the first human - request after an ingest is already hot. Each request is independent; one - failure does not abort the others.""" - - def warm(url: str) -> None: - try: - _http("GET", url, None, timeout) - except Exception as exc: # noqa: BLE001 -- warm is best-effort. - print(f"warning: warm {url} failed: {exc}", file=sys.stderr) - - warm(f"{base}/") - try: - groups_body = _http("GET", f"{base}/api/groups", None, timeout) - slugs = [g["slug"] for g in json.loads(groups_body).get("groups", []) if "slug" in g] - except Exception as exc: # noqa: BLE001 -- group discovery is best-effort. - print(f"warning: warm group discovery failed: {exc}", file=sys.stderr) - return - # A whole-bundle recompute is a few seconds cold, so warm with bounded - # concurrency rather than one slow serial pass. - with ThreadPoolExecutor(max_workers=4) as pool: - pool.map(lambda s: warm(f"{base}/api/group/{s}?n=100"), slugs) - - -def refresh_site_cache(base_url: str, token: str, timeout: float) -> None: - """Revalidate the site's Data Cache tag, then warm the default windows. - - BEST-EFFORT: every failure is logged to stderr and swallowed so a cache - refresh can never change the ingest exit code. The warm pass is skipped - when revalidation fails: warming after a failed flush would repopulate the - Data Cache with stale data, which is the opposite of the intent. - """ - base = base_url.rstrip("/") - try: - _http("POST", f"{base}/api/revalidate", token, timeout) - except Exception as exc: # noqa: BLE001 -- refresh must never raise into ingest. - print(f"warning: cache revalidate failed: {exc}", file=sys.stderr) - return # Skip the warm pass: no point warming a cache that was not flushed. - _warm_default_windows(base, timeout) - - # A restart of the RDS instance refuses connections for 30 seconds to a few minutes: on 2026-10-28 # an engine upgrade took 33 seconds and an OS update about 3 minutes, and four ingest jobs failed. # These waits, about 4 minutes in all, span that window. @@ -1049,12 +987,8 @@ def write() -> tuple[int, int]: separators=(",", ":"), ) ) - # Best-effort site-cache refresh after a successful write. It runs only - # when both environment variables are configured and can never fail ingest. - base_url = os.environ.get("BENCH_SITE_BASE_URL") - revalidate_token = os.environ.get("BENCH_REVALIDATE_TOKEN") - if base_url and revalidate_token: - refresh_site_cache(base_url, revalidate_token, args.timeout) + # The site cache is refreshed once per push by `refresh-bench-site.py`, after the last + # benchmark job of the push, not after each ingest. return 0 diff --git a/scripts/refresh-bench-site.py b/scripts/refresh-bench-site.py new file mode 100755 index 00000000000..af19a66ed47 --- /dev/null +++ b/scripts/refresh-bench-site.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// + +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + +"""Tell the benchmarks website that new results exist, then warm its default pages. + +`develop-bench.yml` runs this once per develop push, after every benchmark job of that push +has finished and ingested its results with `post-ingest.py`. One revalidate per push, rather +than one per benchmark job, keeps the website's Data Cache from being flushed and refilled +dozens of times an hour while benchmarks run. + +The request shape is the contract in `bench/CONTRACT.md` of the vortex-data/vortex.dev +repository: `POST {BENCH_SITE_BASE_URL}/api/revalidate` with +`Authorization: Bearer $BENCH_REVALIDATE_TOKEN`. + +The script does nothing when either environment variable is unset. A revalidate that fails with +a transport error, HTTP 429 or HTTP 5xx is tried again a few times; a revalidate that still fails +exits non-zero. A failed warm request only prints a warning. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +import urllib.error +import urllib.request +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--timeout", + type=float, + default=30.0, + help="Timeout of each HTTP request in seconds (default: 30).", + ) + return parser.parse_args() + + +def _http(method: str, url: str, token: str | None, timeout: float) -> bytes: + """Issue one HTTP request and return the body. Raises on any non-2xx or transport error.""" + headers = {"accept": "application/json"} + if token is not None: + headers["authorization"] = f"Bearer {token}" + req = urllib.request.Request(url, method=method, headers=headers) + with urllib.request.urlopen(req, timeout=timeout) as resp: + return resp.read() + + +def _warm_default_windows(base: str, timeout: float) -> None: + """Best-effort warm pass: request the landing page and every group's default last-100 + bundle, so that the site refreshes them before a person asks for them. Each request is + independent; one failure does not stop the others.""" + + def warm(url: str) -> None: + try: + _http("GET", url, None, timeout) + except Exception as exc: # noqa: BLE001 -- warm is best-effort. + print(f"warning: warm {url} failed: {exc}", file=sys.stderr) + + warm(f"{base}/") + try: + groups_body = _http("GET", f"{base}/api/groups", None, timeout) + slugs = [g["slug"] for g in json.loads(groups_body).get("groups", []) if "slug" in g] + except Exception as exc: # noqa: BLE001 -- group discovery is best-effort. + print(f"warning: warm group discovery failed: {exc}", file=sys.stderr) + return + # A whole-bundle recompute is a few seconds cold, so warm with bounded + # concurrency rather than one slow serial pass. + with ThreadPoolExecutor(max_workers=4) as pool: + pool.map(lambda s: warm(f"{base}/api/group/{s}?n=100"), slugs) + + +# Waits between revalidate attempts. The run sends only this one revalidate, so a short outage +# or redeploy of the website must not leave the run's results behind the old cache. +_RETRY_DELAYS_S = (5, 15, 45) + + +def _is_transient(exc: Exception) -> bool: + """True for a transport error, HTTP 429 or HTTP 5xx. Other HTTP errors, such as 401 for a + wrong token, do not change when the request is sent again.""" + if isinstance(exc, urllib.error.HTTPError): + return exc.code == 429 or exc.code >= 500 + return isinstance(exc, OSError) + + +def refresh_site_cache( + base_url: str, + token: str, + timeout: float, + *, + delays: tuple[float, ...] = _RETRY_DELAYS_S, + sleep: Callable[[float], None] = time.sleep, +) -> bool: + """Revalidate the site's Data Cache tag, then warm the default windows. + + Returns False when the revalidate fails. The warm pass is then skipped: warming a cache + that was not marked stale only keeps the old data. + """ + base = base_url.rstrip("/") + for attempt in range(len(delays) + 1): + try: + _http("POST", f"{base}/api/revalidate", token, timeout) + break + except Exception as exc: # noqa: BLE001 -- reported through the exit code. + if attempt == len(delays) or not _is_transient(exc): + print(f"error: cache revalidate failed: {exc}", file=sys.stderr) + return False + print(f"warning: cache revalidate failed ({exc}); retrying in {delays[attempt]:g}s", file=sys.stderr) + sleep(delays[attempt]) + _warm_default_windows(base, timeout) + return True + + +def main() -> int: + args = parse_args() + base_url = os.environ.get("BENCH_SITE_BASE_URL") + token = os.environ.get("BENCH_REVALIDATE_TOKEN") + if not base_url or not token: + print("BENCH_SITE_BASE_URL or BENCH_REVALIDATE_TOKEN is not set; skipping site refresh.") + return 0 + return 0 if refresh_site_cache(base_url, token, args.timeout) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/tests/test_refresh_bench_site.py b/scripts/tests/test_refresh_bench_site.py new file mode 100644 index 00000000000..63fa94b3731 --- /dev/null +++ b/scripts/tests/test_refresh_bench_site.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + +"""Tests for the revalidate retries of `scripts/refresh-bench-site.py`.""" + +import importlib.util +import urllib.error +from pathlib import Path +from types import ModuleType + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[2] +BASE = "https://bench.example" + + +@pytest.fixture +def refresh() -> ModuleType: + spec = importlib.util.spec_from_file_location("refresh_bench_site", REPO_ROOT / "scripts/refresh-bench-site.py") + assert spec is not None + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def _http_error(code: int) -> urllib.error.HTTPError: + return urllib.error.HTTPError(f"{BASE}/api/revalidate", code, "error", None, None) # ty: ignore[invalid-argument-type] + + +def _fake_site(refresh: ModuleType, monkeypatch: pytest.MonkeyPatch, posts: list[Exception | None]) -> list[str]: + """Replace `_http`. Each POST takes the next entry of `posts`: an error to raise, or None for + success. Every GET succeeds. Returns the log of requests.""" + log: list[str] = [] + + def http(method: str, url: str, token: str | None, timeout: float) -> bytes: + log.append(f"{method} {url.removeprefix(BASE)}") + if method == "POST": + assert token == "token" + outcome = posts.pop(0) + if outcome is not None: + raise outcome + return b'{"revalidated":true}' + if url.endswith("/api/groups"): + return b'{"groups":[{"slug":"a"}]}' + return b"{}" + + monkeypatch.setattr(refresh, "_http", http) + return log + + +@pytest.mark.parametrize( + "error", + [_http_error(503), _http_error(429), urllib.error.URLError("connection refused"), TimeoutError("timed out")], +) +def test_retries_transient_errors_then_warms(refresh, monkeypatch, error): + log = _fake_site(refresh, monkeypatch, [error, error, None]) + sleeps: list[float] = [] + assert refresh.refresh_site_cache(BASE, "token", 1.0, sleep=sleeps.append) + assert sleeps == list(refresh._RETRY_DELAYS_S[:2]) + assert log == [ + "POST /api/revalidate", + "POST /api/revalidate", + "POST /api/revalidate", + "GET /", + "GET /api/groups", + "GET /api/group/a?n=100", + ] + + +def test_does_not_retry_an_unauthorized_token(refresh, monkeypatch): + log = _fake_site(refresh, monkeypatch, [_http_error(401)]) + sleeps: list[float] = [] + assert not refresh.refresh_site_cache(BASE, "token", 1.0, sleep=sleeps.append) + assert sleeps == [] + assert log == ["POST /api/revalidate"] + + +def test_fails_without_warming_when_retries_run_out(refresh, monkeypatch): + attempts = len(refresh._RETRY_DELAYS_S) + 1 + log = _fake_site(refresh, monkeypatch, [_http_error(502)] * attempts) + sleeps: list[float] = [] + assert not refresh.refresh_site_cache(BASE, "token", 1.0, sleep=sleeps.append) + assert sleeps == list(refresh._RETRY_DELAYS_S) + assert log == ["POST /api/revalidate"] * attempts From 8b01ab55bf9383a97e7ccc8ebaf046030c256898 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 10:43:54 -0500 Subject: [PATCH 20/41] Peel Arrow dictionaries before Arrow importer plugins run (#11185) ## Summary - Tracking Issue: #11114 A dictionary-encoded extension column imports today only because its importer declines the `Dictionary` type, after which `ArrowSession` retries with the value type. The next PR in this stack (SN2 in #11114) makes the importers of known types fail on Arrow input outside their type instead of declining it, which would break these columns. So the session now peels the dictionary before any importer runs. The result is still a `Dict` over the imported values, so nothing is decompressed. ## Changes The existing dictionary test now also runs with an importer that fails on any dictionary. ## API Changes `ArrowImportVTable` implementations no longer receive `Dictionary` fields or arrays. They get the value field, with the dictionary field's metadata, and the values array. Note that a dictionary-encoded GeoArrow field with a CRS now keeps its CRS in the dtype from `ArrowSession::from_arrow_field` and `from_arrow_schema`, which matches what the array import already produced. Before, the name-only fallback of the spatial importers accepted the dictionary field itself and used default metadata. Signed-off-by: Connor Tsui --- vortex-arrow/src/session.rs | 139 ++++++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 30 deletions(-) diff --git a/vortex-arrow/src/session.rs b/vortex-arrow/src/session.rs index 32108a4f599..ec74a598ea7 100644 --- a/vortex-arrow/src/session.rs +++ b/vortex-arrow/src/session.rs @@ -29,6 +29,7 @@ use arrow_array::Array as ArrowArray; use arrow_array::ArrayRef as ArrowArrayRef; use arrow_array::RecordBatch; use arrow_array::RunArray; +use arrow_array::cast::AsArray; use arrow_array::make_array; use arrow_array::types::Int16Type; use arrow_array::types::Int32Type; @@ -185,7 +186,10 @@ pub trait ArrowExportVTable: 'static + Send + Sync + Debug { /// Plugin layer for importing an Arrow extension-typed array into a Vortex array. /// -/// Plugins are dispatched by `arrow_ext_id`. +/// Plugins are dispatched by `arrow_ext_id`. The session peels Arrow dictionaries before it +/// dispatches, so a plugin never sees a [`DataType::Dictionary`] field or array. It sees the value +/// field, with the dictionary field's metadata, and the values array. The session then wraps the +/// plugin's result in a [`DictArray`] with the dictionary's codes. /// /// This is purely an implementation trait, its methods should not be called directly. Instead, /// use the methods on [`ArrowSession`]. @@ -391,6 +395,8 @@ impl ArrowSession { /// Build the Vortex [`DType`] for an Arrow [`Field`]. /// + /// A [`DataType::Dictionary`] field converts as its value field with the same metadata. + /// /// Plugins registered against the field's Arrow extension name are tried in /// registration order; the first plugin to return `Some(dtype)` wins. If none /// match (or all return `None`), the builtin `arrow.parquet.variant` extension maps @@ -399,6 +405,11 @@ impl ArrowSession { /// fields is preserved. #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")] pub fn from_arrow_field(&self, field: &Field) -> VortexResult { + if let DataType::Dictionary(_, value_type) = field.data_type() { + return self + .from_arrow_field(&field.clone().with_data_type(value_type.as_ref().clone())); + } + if let Some(name) = field.metadata().get(EXTENSION_TYPE_NAME_KEY) { for plugin in self.importers(&Id::new(name)).iter() { if let Some(dtype) = plugin.from_arrow_field(field, self)? { @@ -410,10 +421,6 @@ impl ArrowSession { return Ok(DType::Variant(field.is_nullable().into())); } } - if let DataType::Dictionary(_, value_type) = field.data_type() { - return self - .from_arrow_field(&field.clone().with_data_type(value_type.as_ref().clone())); - } self.from_arrow_datatype(field.data_type(), field.is_nullable().into()) } @@ -596,6 +603,9 @@ impl ArrowSession { /// extension fields nested inside containers reach their importers; leaf types fall /// through to the canonical Arrow → Vortex array conversion. /// + /// A dictionary array imports as a [`DictArray`]. Its values import as described above, with + /// `field`'s metadata. + /// /// # Errors /// /// Returns an error if the field (or requested nullability) is non-nullable but the array @@ -609,15 +619,20 @@ impl ArrowSession { self.from_arrow_array_inner(array, field.as_ref()) } - /// [`Self::from_arrow_array`] with the Arrow [`Field`] already resolved: probe the import - /// plugins registered for the field's extension name, then fall back to the canonical - /// conversion. Also the recursion point for nested fields, which already have a [`Field`]. + /// [`Self::from_arrow_array`] with the Arrow [`Field`] already resolved: peel a dictionary, + /// probe the import plugins registered for the field's extension name, then fall back to the + /// canonical conversion. Also the recursion point for nested fields, which already have a + /// [`Field`]. #[allow(clippy::wrong_self_convention)] fn from_arrow_array_inner( &self, array: ArrowArrayRef, field: &Field, ) -> VortexResult { + if let DataType::Dictionary(..) = field.data_type() { + return self.dictionary_from_arrow(array.as_ref(), field); + } + if let Some(extension_name) = field.metadata().get(EXTENSION_TYPE_NAME_KEY) { #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")] let importers = self.importers(&Id::new(extension_name)); @@ -668,8 +683,6 @@ impl ArrowSession { array: &dyn ArrowArray, field: &Field, ) -> VortexResult { - use arrow_array::cast::AsArray; - match field.data_type() { DataType::Struct(fields) => { let arrow_struct = array.as_struct(); @@ -790,20 +803,6 @@ impl ArrowSession { ), } } - DataType::Dictionary(..) => { - let dict = array.as_any_dictionary(); - // Extension metadata on the dictionary field describes its values. - let values_field = field - .clone() - .with_data_type(dict.values().data_type().clone()); - let values = - self.from_arrow_array(ArrowArrayRef::clone(dict.values()), &values_field)?; - let codes = dict.keys(); - let codes = from_arrow_dyn(codes, codes.is_nullable())?; - // SAFETY: arrow-rs enforces the dictionary invariants on construction, so the - // codes are in-bounds for the values. - Ok(unsafe { DictArray::new_unchecked(codes, values) }.into_array()) - } _ => from_arrow_dyn(array, field.is_nullable()), } } @@ -827,6 +826,34 @@ impl ArrowSession { self.from_arrow_array_inner(ArrowArrayRef::clone(run_array.values()), values_field)?; run_end_from_arrow(run_array, values) } + + /// Decode an Arrow dictionary array into a [`DictArray`], importing its values with `field`'s + /// metadata so that an extension name on the dictionary field reaches its importer. + fn dictionary_from_arrow( + &self, + array: &dyn ArrowArray, + field: &Field, + ) -> VortexResult { + let dict = array.as_any_dictionary_opt().ok_or_else(|| { + vortex_err!( + "expected an Arrow DictionaryArray, got {}", + array.data_type() + ) + })?; + + let values_field = field + .clone() + .with_data_type(dict.values().data_type().clone()); + let values = + self.from_arrow_array_inner(ArrowArrayRef::clone(dict.values()), &values_field)?; + + let codes = dict.keys(); + let codes = from_arrow_dyn(codes, codes.is_nullable())?; + + // SAFETY: arrow-rs enforces the dictionary invariants on construction, so the codes are + // in-bounds for the values. + Ok(unsafe { DictArray::new_unchecked(codes, values) }.into_array()) + } } /// The values field of an Arrow [`DataType::RunEndEncoded`], re-stamped with the run-end array's @@ -1072,16 +1099,69 @@ mod tests { Ok(()) } - /// A plain Arrow dictionary imports as a Vortex `Dict` array over the dictionary values, - /// matching the dtype the schema conversion reports. - #[test] - fn dictionary_imports_as_dict_encoding() -> VortexResult<()> { + static REJECT_DICTIONARY_ID: CachedId = CachedId::new("test.reject_dictionary"); + + /// An importer that fails on an Arrow dictionary and imports anything else as its storage. + #[derive(Debug)] + struct DictionaryRejectingImporter; + + impl ArrowImportVTable for DictionaryRejectingImporter { + fn arrow_ext_id(&self) -> Id { + *REJECT_DICTIONARY_ID + } + + fn from_arrow_field( + &self, + field: &Field, + session: &ArrowSession, + ) -> VortexResult> { + vortex_ensure!( + !matches!(field.data_type(), DataType::Dictionary(..)), + "an importer must never see a dictionary field, got {}", + field.data_type() + ); + session + .from_arrow_datatype(field.data_type(), field.is_nullable().into()) + .map(Some) + } + + fn from_arrow_array( + &self, + array: ArrowArrayRef, + field: &Field, + _dtype: &DType, + session: &ArrowSession, + ) -> VortexResult { + vortex_ensure!( + !matches!(array.data_type(), DataType::Dictionary(..)), + "an importer must never see a dictionary array, got {}", + array.data_type() + ); + session + .from_arrow_array(array, field.is_nullable()) + .map(ArrowImport::Imported) + } + } + + /// An Arrow dictionary imports as a Vortex `Dict` array over the dictionary values, matching + /// the dtype the schema conversion reports. The session peels the dictionary before any + /// importer runs, so a dictionary field named for an importer that fails on dictionaries still + /// imports. + #[rstest] + #[case::plain(None)] + #[case::extension_name(Some(*REJECT_DICTIONARY_ID))] + fn dictionary_imports_as_dict_encoding(#[case] extension_name: Option) -> VortexResult<()> { let session = ArrowSession::default(); + session.register_importer(Arc::new(DictionaryRejectingImporter)); let dict: ArrowArrayRef = Arc::new(DictionaryArray::::try_new( Int32Array::from(vec![0, 1, 0, 1]), Arc::new(StringArray::from(vec!["a", "b"])), )?); - let field = Field::new("s", dict.data_type().clone(), false); + let metadata = extension_name + .map(|id| (EXTENSION_TYPE_NAME_KEY.to_string(), id.to_string())) + .into_iter() + .collect(); + let field = Field::new("s", dict.data_type().clone(), false).with_metadata(metadata); let dtype = session.from_arrow_field(&field)?; assert_eq!(dtype, DType::Utf8(Nullability::NonNullable)); @@ -1089,7 +1169,6 @@ mod tests { let array = session.from_arrow_array(dict, &field)?; assert!(array.is::(), "expected a Dict encoding, got {array}"); assert_eq!(array.dtype(), &dtype); - assert_eq!(array.len(), 4); assert_arrays_eq!( array, VarBinViewArray::from_iter_str(["a", "b", "a", "b"]), From 8dcf8d3dc574b9609bf3f64ede34ae0f27acc3d7 Mon Sep 17 00:00:00 2001 From: Joe Isaacs Date: Wed, 4 Nov 2026 16:34:54 +0000 Subject: [PATCH 21/41] Count each run's selected rows with one pass over the RunEnd filter mask (#11221) Signed-off-by: Joe Isaacs --- .../src/arrays/runend/compute/filter.rs | 110 ++++++++++++++++-- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/vortex-array/src/arrays/runend/compute/filter.rs b/vortex-array/src/arrays/runend/compute/filter.rs index 9170690316b..172908051ff 100644 --- a/vortex-array/src/arrays/runend/compute/filter.rs +++ b/vortex-array/src/arrays/runend/compute/filter.rs @@ -7,6 +7,7 @@ use std::ops::AddAssign; use num_traits::AsPrimitive; use num_traits::NumCast; use vortex_buffer::BitBuffer; +use vortex_buffer::BitChunkIterator; use vortex_buffer::BufferAllocatorRef; use vortex_buffer::BufferMut; use vortex_error::VortexExpect; @@ -109,22 +110,21 @@ pub fn filter_run_end_primitive + AsPrim ) -> VortexResult<(PrimitiveArray, Mask)> { let mut filtered_run_ends = BufferMut::::zeroed_in(run_ends.len(), allocator.clone()); - let mut run_start = 0u64; let mut retained_run_count = 0; let mut filtered_end = R::zero(); + let mut rank = MaskRank::new(mask); + let mut selected_before_run = 0; let values_mask: Mask = BitBuffer::collect_bool(run_ends.len(), |run_idx| { let run_end = min(run_ends[run_idx].as_() - offset, length); - - // Bulk popcount is SIMD-capable and avoids per-bit reads. The input contract and clamp prove - // `run_start_idx <= run_end_idx <= mask.len()`. - let run_start_idx = run_start - .try_into() - .vortex_expect("run start index must fit in usize"); let run_end_idx = run_end .try_into() .vortex_expect("run end index must fit in usize"); - let selected_in_run = mask.count_range(run_start_idx, run_end_idx); + // Run ends increase, so the rank walks the mask's words once over all runs instead of + // counting each run's range from scratch, which dominates when runs are short. + let selected_through_run = rank.rank(run_end_idx); + let selected_in_run = selected_through_run - selected_before_run; + selected_before_run = selected_through_run; filtered_end += ::from(selected_in_run) .vortex_expect("run popcount must fit in run-end native type"); let retain_run = selected_in_run > 0; @@ -134,7 +134,6 @@ pub fn filter_run_end_primitive + AsPrim filtered_run_ends[retained_run_count] = filtered_end; retained_run_count += retain_run as usize; - run_start = run_end; retain_run }) .into(); @@ -147,8 +146,55 @@ pub fn filter_run_end_primitive + AsPrim )) } +/// Counts the set bits of a mask before non-decreasing positions, reading each 64-bit word once. +struct MaskRank<'a> { + words: BitChunkIterator<'a>, + remainder: u64, + /// The index of `word` among the mask's words. + word_idx: usize, + word: u64, + /// The set bits in the words before `word`. + before_word: usize, +} + +impl<'a> MaskRank<'a> { + fn new(mask: &'a BitBuffer) -> Self { + let chunks = mask.chunks(); + let remainder = chunks.remainder_bits(); + let mut words = chunks.iter(); + let word = words.next().unwrap_or(remainder); + Self { + words, + remainder, + word_idx: 0, + word, + before_word: 0, + } + } + + /// The set bits in `0..pos`, for `pos` no smaller than in the previous call. + #[inline] + fn rank(&mut self, pos: usize) -> usize { + let word_idx = pos / 64; + while self.word_idx < word_idx { + self.before_word += self.word.count_ones() as usize; + self.word_idx += 1; + self.word = self.words.next().unwrap_or(self.remainder); + } + let bits = pos % 64; + let low = if bits == 0 { + 0 + } else { + u64::MAX >> (64 - bits) + }; + self.before_word + (self.word & low).count_ones() as usize + } +} + #[cfg(test)] mod tests { + use rstest::rstest; + use vortex_buffer::BitBuffer; use vortex_error::VortexResult; use vortex_mask::Mask; @@ -157,6 +203,7 @@ mod tests { use crate::arrays::PrimitiveArray; use crate::arrays::runend::RunEnd; use crate::arrays::runend::RunEndArray; + use crate::arrays::runend::compute::filter::filter_run_end_primitive; use crate::arrays::runend::tests::SESSION; use crate::assert_arrays_eq; @@ -217,4 +264,49 @@ mod tests { Ok(()) } + + /// The rank walk must count the same rows per run as counting each run's range, for masks + /// that start inside a byte and end inside or at the end of a word. + #[rstest] + #[case(0, 128)] + #[case(3, 128)] + #[case(5, 200)] + #[case(61, 64)] + #[case(7, 1)] + fn short_runs_count_each_runs_selected_rows( + #[case] mask_offset: usize, + #[case] len: usize, + ) -> VortexResult<()> { + let bits = BitBuffer::from_iter((0..mask_offset + len).map(|i| i % 3 != 0 || i % 7 == 0)); + let mask = bits.slice(mask_offset..mask_offset + len); + // Runs of 1, 2 and 3 rows. + let mut run_ends = Vec::new(); + let mut end = 0u32; + let mut run_len = 1; + while (end as usize) < len { + end = (end + run_len).min(u32::try_from(len)?); + run_ends.push(end); + run_len = run_len % 3 + 1; + } + + let ctx = SESSION.create_execution_ctx(); + let (filtered_ends, values_mask) = + filter_run_end_primitive(&run_ends, 0, len as u64, &mask, ctx.allocator())?; + + let mut expected_ends = Vec::new(); + let mut expected_values = Vec::new(); + let (mut start, mut selected) = (0, 0u32); + for &end in &run_ends { + let in_run = mask.count_range(start, end as usize); + selected += u32::try_from(in_run)?; + expected_values.push(in_run > 0); + if in_run > 0 { + expected_ends.push(selected); + } + start = end as usize; + } + assert_eq!(filtered_ends.as_slice::(), expected_ends.as_slice()); + assert_eq!(values_mask, Mask::from_iter(expected_values)); + Ok(()) + } } From 8863f85984f1d57ee5e5555ce2b8d386c2630b3f Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Wed, 4 Nov 2026 16:37:14 +0000 Subject: [PATCH 22/41] Avoid allocating Vec of chunks just to copy it in ChunkedArray constructor (#9922) We fix cases where we were passing a vec that was immediately deallocated by passing an impl IntoIterator where possible For other cases we add a new constructor that leverages process_results in the caller and a size hint to avoid materialising list of items just to check the error case Signed-off-by: Robert Kruszewski --- .../fastlanes/benches/canonicalize_bench.rs | 8 +- vortex-array/benches/take_chunked.rs | 15 +- .../src/aggregate_fn/accumulator_grouped.rs | 14 +- vortex-array/src/arrays/chunked/array.rs | 139 +++++++++++++++--- .../src/arrays/chunked/compute/cast.rs | 20 ++- .../src/arrays/chunked/compute/fill_null.rs | 23 +-- .../src/arrays/chunked/compute/mask.rs | 30 ++-- .../src/arrays/chunked/compute/rules.rs | 70 ++++----- .../src/arrays/chunked/compute/slice.rs | 24 ++- .../src/arrays/chunked/compute/zip.rs | 25 ++-- .../src/arrays/chunked/vtable/canonical.rs | 43 +++--- .../src/arrays/constant/vtable/canonical.rs | 5 +- vortex-array/src/arrays/dict/compute/rules.rs | 5 +- .../src/arrays/listview/compute/zip.rs | 15 +- .../src/arrays/variant/vtable/kernel.rs | 16 +- vortex-array/src/builders/child.rs | 10 +- vortex-array/src/builders/fixed_size_list.rs | 2 +- vortex-array/src/iter.rs | 29 +++- .../src/scalar_fn/fns/variant_get/mod.rs | 16 +- vortex-bench/src/conversions.rs | 16 +- vortex-bench/src/vector_dataset/convert.rs | 22 ++- vortex-compressor/src/compressor/sample.rs | 13 +- vortex-file/benches/split_collection.rs | 25 ++-- vortex-python/src/arrays/from_arrow.rs | 63 +++++--- 24 files changed, 403 insertions(+), 245 deletions(-) diff --git a/encodings/fastlanes/benches/canonicalize_bench.rs b/encodings/fastlanes/benches/canonicalize_bench.rs index a4ff901c23f..0fa743493f2 100644 --- a/encodings/fastlanes/benches/canonicalize_bench.rs +++ b/encodings/fastlanes/benches/canonicalize_bench.rs @@ -90,7 +90,7 @@ fn into_canonical_non_nullable( bencher .with_inputs(|| { - let chunked = ChunkedArray::from_iter(chunks.clone()).into_array(); + let chunked = ChunkedArray::from_iter(chunks.iter().cloned()).into_array(); mark_decoded(&chunked); (chunked, SESSION.create_execution_ctx()) @@ -123,7 +123,7 @@ fn canonical_into_non_nullable( bencher .with_inputs(|| { - let chunked = ChunkedArray::from_iter(chunks.clone()).into_array(); + let chunked = ChunkedArray::from_iter(chunks.iter().cloned()).into_array(); let ctx = SESSION.create_execution_ctx(); let primitive_builder = PrimitiveBuilder::::with_capacity_in( chunked.dtype().nullability(), @@ -184,7 +184,7 @@ fn into_canonical_nullable( bencher .with_inputs(|| { - let chunked = ChunkedArray::from_iter(chunks.clone()).into_array(); + let chunked = ChunkedArray::from_iter(chunks.iter().cloned()).into_array(); mark_decoded(&chunked); (chunked, SESSION.create_execution_ctx()) @@ -217,7 +217,7 @@ fn canonical_into_nullable( bencher .with_inputs(|| { - let chunked = ChunkedArray::from_iter(chunks.clone()).into_array(); + let chunked = ChunkedArray::from_iter(chunks.iter().cloned()).into_array(); let ctx = SESSION.create_execution_ctx(); let primitive_builder = PrimitiveBuilder::::with_capacity_in( chunked.dtype().nullability(), diff --git a/vortex-array/benches/take_chunked.rs b/vortex-array/benches/take_chunked.rs index 592a495c5ed..443438657cf 100644 --- a/vortex-array/benches/take_chunked.rs +++ b/vortex-array/benches/take_chunked.rs @@ -244,15 +244,12 @@ fn chunk_values(value_kind: ValueKind, start: usize, end: usize) -> ArrayRef { fn chunked_values(case: Case) -> ArrayRef { let chunk_len = SOURCE_LEN / case.chunks; - let chunks = (0..case.chunks) - .map(|chunk_idx| { - let start = chunk_idx * chunk_len; - let end = start + chunk_len; - chunk_values(case.value_kind, start, end) - }) - .collect::>(); - let dtype = chunks[0].dtype().clone(); - ChunkedArray::try_new(chunks, dtype).unwrap().into_array() + let chunks = (0..case.chunks).map(|chunk_idx| { + let start = chunk_idx * chunk_len; + let end = start + chunk_len; + chunk_values(case.value_kind, start, end) + }); + ChunkedArray::from_iter(chunks).into_array() } fn advance_random(state: &mut u64) -> u64 { diff --git a/vortex-array/src/aggregate_fn/accumulator_grouped.rs b/vortex-array/src/aggregate_fn/accumulator_grouped.rs index 832f4a06f78..4fd8aff18dc 100644 --- a/vortex-array/src/aggregate_fn/accumulator_grouped.rs +++ b/vortex-array/src/aggregate_fn/accumulator_grouped.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use std::mem; + use arrow_buffer::ArrowNativeType; use vortex_buffer::Buffer; use vortex_error::VortexExpect; @@ -260,11 +262,15 @@ impl DynGroupedAccumulator for GroupedAccumulator { } fn flush(&mut self) -> VortexResult { - let mut states = std::mem::take(&mut self.partials); - if states.len() == 1 { - return Ok(states.pop().vortex_expect("checked one partial")); + if self.partials.len() == 1 { + return Ok(self.partials.pop().vortex_expect("checked one partial")); } - Ok(ChunkedArray::try_new(states, self.dtypes.partial_dtype.clone())?.into_array()) + // Pass the `Vec` itself so that this shares the `try_new` instance of other callers. + Ok(ChunkedArray::try_new( + mem::take(&mut self.partials), + self.dtypes.partial_dtype.clone(), + )? + .into_array()) } fn finish(&mut self) -> VortexResult { diff --git a/vortex-array/src/arrays/chunked/array.rs b/vortex-array/src/arrays/chunked/array.rs index 3121da57d3a..ca0cfc751e6 100644 --- a/vortex-array/src/arrays/chunked/array.rs +++ b/vortex-array/src/arrays/chunked/array.rs @@ -164,34 +164,56 @@ impl Array { ) -> VortexResult> { let chunks = chunks.into_iter(); let (lower, _) = chunks.size_hint(); - let mut slots = ArraySlots::with_capacity(ChunkedSlots::CHUNKS_OFFSET + lower); + Self::parts_from_chunks_with_capacity::(chunks, dtype, lower) + } + + fn parts_from_chunks_with_capacity( + chunks: impl IntoIterator, + dtype: DType, + capacity: usize, + ) -> VortexResult> { + let mut slots = ArraySlots::with_capacity(ChunkedSlots::CHUNKS_OFFSET + capacity); slots.push(None); - let mut chunk_offsets = Vec::with_capacity(lower + 1); + let mut chunk_offsets = Vec::with_capacity(capacity + 1); chunk_offsets.push(0); let mut len = 0usize; for chunk in chunks { - if VALIDATE && chunk.dtype() != &dtype { - vortex_bail!( - "expected type: {} but instead got {}", - &dtype, - chunk.dtype() - ); + if VALIDATE { + Self::validate_chunk_dtype(&chunk, &dtype)?; } len += chunk.len(); chunk_offsets.push(len); slots.push(Some(chunk)); } + Ok(Self::parts_from_slots(dtype, len, chunk_offsets, slots)) + } + + /// Checks that the chunk has the dtype of the array. + /// + /// This function is not generic, so each iterator type does not compile its own copy of the + /// error message. + fn validate_chunk_dtype(chunk: &ArrayRef, dtype: &DType) -> VortexResult<()> { + if chunk.dtype() != dtype { + vortex_bail!("expected type: {} but instead got {}", dtype, chunk.dtype()); + } + Ok(()) + } + + /// Creates the parts from the collected chunk slots and offsets. + /// + /// This function is not generic, so each iterator type that `parts_from_chunks_with_capacity` + /// gets compiled for does not compile its own copy. + fn parts_from_slots( + dtype: DType, + len: usize, + chunk_offsets: Vec, + mut slots: ArraySlots, + ) -> ArrayParts { slots[ChunkedSlots::CHUNK_OFFSETS] = Some(ChunkedData::make_chunk_offsets_array(&chunk_offsets)); - Ok(ArrayParts::new( - Chunked, - dtype, - len, - ChunkedData::new(chunk_offsets), - slots, - )) + ArrayParts::new(Chunked, dtype, len, ChunkedData::new(chunk_offsets), slots) } pub(super) fn with_next_builder_slot(mut self, next_builder_slot: usize) -> Self { @@ -241,7 +263,10 @@ impl Array { && !chunks_to_combine.is_empty() { let canonical = unsafe { - Array::::new_unchecked(chunks_to_combine, self.dtype().clone()) + Array::::new_unchecked( + chunks_to_combine.drain(..), + self.dtype().clone(), + ) } .into_array() .execute::(ctx)? @@ -250,7 +275,6 @@ impl Array { new_chunk_n_bytes = 0; new_chunk_n_elements = 0; - chunks_to_combine = Vec::new(); } if n_bytes > target_bytesize || n_elements > target_rowsize { @@ -280,8 +304,28 @@ impl Array { /// /// All chunks must have exactly the same [`DType`] as the provided `dtype`. pub unsafe fn new_unchecked(chunks: impl IntoIterator, dtype: DType) -> Self { - let parts = Self::parts_from_chunks::(chunks, dtype) + let chunks = chunks.into_iter(); + let (expected_nchunks, _) = chunks.size_hint(); + // SAFETY: the caller guarantees the dtype of every chunk. + unsafe { Self::new_unchecked_sized(chunks, dtype, expected_nchunks) } + } + + /// Creates a chunked array without validation, reserving space for `expected_nchunks` chunks. + /// + /// The expected count is only an allocation hint: fewer or more chunks are allowed. + /// This is useful when iterator adapters such as `process_results` lose the source size hint. + /// + /// # Safety + /// + /// All chunks must have exactly the same [`DType`] as the provided `dtype`. + pub unsafe fn new_unchecked_sized( + chunks: impl IntoIterator, + dtype: DType, + expected_nchunks: usize, + ) -> Self { + let parts = Self::parts_from_chunks_with_capacity::(chunks, dtype, expected_nchunks) .vortex_expect("unchecked chunked construction cannot fail"); + // SAFETY: the caller guarantees the dtype of every chunk. unsafe { Self::from_chunked_parts_unchecked(parts) } } @@ -308,9 +352,9 @@ impl Array { impl FromIterator for Array { fn from_iter>(iter: T) -> Self { - let chunks: Vec = iter.into_iter().collect(); + let mut chunks = iter.into_iter().peekable(); let dtype = chunks - .first() + .peek() .map(|c| c.dtype().clone()) .vortex_expect("Cannot infer DType from an empty iterator"); Array::::try_new(chunks, dtype) @@ -322,8 +366,11 @@ impl FromIterator for Array { mod test { use std::sync::LazyLock; + use itertools::Itertools; + use rstest::rstest; use vortex_buffer::buffer; use vortex_error::VortexResult; + use vortex_error::vortex_err; use vortex_session::VortexSession; use crate::IntoArray; @@ -339,6 +386,58 @@ mod test { static SESSION: LazyLock = LazyLock::new(VortexSession::empty); + #[rstest] + fn sized_chunks( + #[values(0, 1, 17)] nchunks: usize, + #[values(0, 1, 17)] expected_nchunks: usize, + ) -> VortexResult<()> { + let dtype = DType::from(PType::U64); + let chunks = (0..nchunks as u64).map(|i| buffer![i].into_array()); + // SAFETY: every chunk contains non-nullable u64 values. + let array = + unsafe { ChunkedArray::new_unchecked_sized(chunks, dtype.clone(), expected_nchunks) }; + + assert_eq!(array.dtype(), &dtype); + assert_eq!(array.nchunks(), nchunks); + assert_eq!( + array.chunk_offset_values(), + (0..=nchunks).collect::>() + ); + let mut ctx = SESSION.create_execution_ctx(); + assert_arrays_eq!( + array, + PrimitiveArray::from_iter(0..nchunks as u64), + &mut ctx + ); + Ok(()) + } + + #[rstest] + fn sized_chunks_with_process_results(#[values(0, 1)] error_at: usize) { + let mut visited = 0; + let chunks = (0..3).map(|i| { + visited += 1; + if i == error_at { + Err(vortex_err!("chunk source failed")) + } else { + Ok(buffer![1u64].into_array()) + } + }); + let dtype = DType::from(PType::U64); + let result = chunks.process_results(|chunks| { + // SAFETY: every successfully yielded chunk contains non-nullable u64 values. + unsafe { ChunkedArray::new_unchecked_sized(chunks, dtype, 3) } + }); + + assert!( + result + .unwrap_err() + .to_string() + .contains("chunk source failed") + ); + assert_eq!(visited, error_at + 1); + } + #[test] fn test_rechunk_one_chunk() { let mut ctx = SESSION.create_execution_ctx(); diff --git a/vortex-array/src/arrays/chunked/compute/cast.rs b/vortex-array/src/arrays/chunked/compute/cast.rs index 6f52c792f91..e3acc717192 100644 --- a/vortex-array/src/arrays/chunked/compute/cast.rs +++ b/vortex-array/src/arrays/chunked/compute/cast.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use itertools::Itertools; use vortex_error::VortexResult; use crate::ArrayRef; @@ -15,17 +16,14 @@ use crate::scalar_fn::fns::cast::CastReduce; impl CastReduce for Chunked { fn cast(array: ArrayView<'_, Chunked>, dtype: &DType) -> VortexResult> { - let mut cast_chunks = Vec::new(); - for chunk in array.iter_chunks() { - cast_chunks.push(chunk.cast(dtype.clone())?); - } - - // SAFETY: casting all chunks retains all chunks have same DType - unsafe { - Ok(Some( - ChunkedArray::new_unchecked(cast_chunks, dtype.clone()).into_array(), - )) - } + let chunks = array.iter_chunks().map(|chunk| chunk.cast(dtype.clone())); + chunks.process_results(|chunks| { + // SAFETY: every chunk is cast to the requested dtype. + Some(unsafe { + ChunkedArray::new_unchecked_sized(chunks, dtype.clone(), array.nchunks()) + .into_array() + }) + }) } } diff --git a/vortex-array/src/arrays/chunked/compute/fill_null.rs b/vortex-array/src/arrays/chunked/compute/fill_null.rs index 7d05a2914e6..df9b864ba0d 100644 --- a/vortex-array/src/arrays/chunked/compute/fill_null.rs +++ b/vortex-array/src/arrays/chunked/compute/fill_null.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use itertools::Itertools; use vortex_error::VortexResult; use crate::ArrayRef; @@ -18,16 +19,18 @@ impl FillNullReduce for Chunked { array: ArrayView<'_, Chunked>, fill_value: &Scalar, ) -> VortexResult> { - let new_chunks = array - .iter_chunks() - .map(|c| c.fill_null(fill_value.clone())) - .collect::>>()?; - - // SAFETY: wrapping each chunk in ScalarFnArray preserves the same DType across all chunks. - Ok(Some( - unsafe { ChunkedArray::new_unchecked(new_chunks, fill_value.dtype().clone()) } - .into_array(), - )) + let chunks = array.iter_chunks().map(|c| c.fill_null(fill_value.clone())); + chunks.process_results(|chunks| { + // SAFETY: filling nulls gives every chunk the fill value's dtype. + Some(unsafe { + ChunkedArray::new_unchecked_sized( + chunks, + fill_value.dtype().clone(), + array.nchunks(), + ) + .into_array() + }) + }) } } diff --git a/vortex-array/src/arrays/chunked/compute/mask.rs b/vortex-array/src/arrays/chunked/compute/mask.rs index f99d4254780..da0ea6b975c 100644 --- a/vortex-array/src/arrays/chunked/compute/mask.rs +++ b/vortex-array/src/arrays/chunked/compute/mask.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use itertools::Itertools; use vortex_error::VortexResult; use crate::ArrayRef; @@ -20,20 +21,23 @@ impl MaskKernel for Chunked { _ctx: &mut ExecutionCtx, ) -> VortexResult> { let chunk_offsets = array.chunk_offset_values(); - let new_chunks: Vec = array - .iter_chunks() - .enumerate() - .map(|(i, chunk)| { - let start = chunk_offsets[i]; - let end = chunk_offsets[i + 1]; - let chunk_mask = mask.slice(start..end)?; - MaskExpr::try_new(chunk.clone(), chunk_mask).map(IntoArray::into_array) + let chunks = array.iter_chunks().enumerate().map(|(i, chunk)| { + let start = chunk_offsets[i]; + let end = chunk_offsets[i + 1]; + let chunk_mask = mask.slice(start..end)?; + MaskExpr::try_new(chunk.clone(), chunk_mask).map(IntoArray::into_array) + }); + chunks.process_results(|chunks| { + // SAFETY: masking makes every chunk's dtype nullable without changing its logical type. + Some(unsafe { + ChunkedArray::new_unchecked_sized( + chunks, + array.dtype().as_nullable(), + array.nchunks(), + ) + .into_array() }) - .collect::>()?; - - Ok(Some( - ChunkedArray::try_new(new_chunks, array.dtype().as_nullable())?.into_array(), - )) + }) } } diff --git a/vortex-array/src/arrays/chunked/compute/rules.rs b/vortex-array/src/arrays/chunked/compute/rules.rs index c5f904910f2..48452a1bc0b 100644 --- a/vortex-array/src/arrays/chunked/compute/rules.rs +++ b/vortex-array/src/arrays/chunked/compute/rules.rs @@ -46,18 +46,18 @@ impl ArrayParentReduceRule for ChunkedUnaryScalarFnPushDownRule { return Ok(None); } - let new_chunks: Vec<_> = array - .iter_chunks() - .map(|chunk| { - ScalarFnArray::try_new(parent.scalar_fn().clone(), vec![chunk.clone()])? + let chunks = array.iter_chunks().map(|chunk| { + ScalarFnArray::try_new(parent.scalar_fn().clone(), vec![chunk.clone()])? + .into_array() + .optimize() + }); + chunks.process_results(|chunks| { + // SAFETY: applying the same scalar function gives every chunk the parent's dtype. + Some(unsafe { + ChunkedArray::new_unchecked_sized(chunks, parent.dtype().clone(), array.nchunks()) .into_array() - .optimize() }) - .try_collect()?; - - Ok(Some( - unsafe { ChunkedArray::new_unchecked(new_chunks, parent.dtype().clone()) }.into_array(), - )) + }) } } @@ -83,33 +83,33 @@ impl ArrayParentReduceRule for ChunkedConstantScalarFnPushDownRule { } let mut offset = 0; - let new_chunks: Vec<_> = array - .iter_chunks() - .map(|chunk| { - let range = offset..offset + chunk.len(); - offset += chunk.len(); - // Each sibling is sliced to the chunk's rows, so that every chunk shares what the - // sibling shares rather than getting its own copy of the constant. - let new_children: Vec<_> = parent - .iter_children() - .enumerate() - .map(|(idx, child)| { - if idx == child_idx { - Ok(chunk.clone()) - } else { - child.slice(range.clone()) - } - }) - .try_collect()?; + let chunks = array.iter_chunks().map(|chunk| { + let range = offset..offset + chunk.len(); + offset += chunk.len(); + // Each sibling is sliced to the chunk's rows, so that every chunk shares what the + // sibling shares rather than getting its own copy of the constant. + let new_children: Vec<_> = parent + .iter_children() + .enumerate() + .map(|(idx, child)| { + if idx == child_idx { + Ok(chunk.clone()) + } else { + child.slice(range.clone()) + } + }) + .try_collect()?; - ScalarFnArray::try_new(parent.scalar_fn().clone(), new_children)? + ScalarFnArray::try_new(parent.scalar_fn().clone(), new_children)? + .into_array() + .optimize() + }); + chunks.process_results(|chunks| { + // SAFETY: applying the same scalar function gives every chunk the parent's dtype. + Some(unsafe { + ChunkedArray::new_unchecked_sized(chunks, parent.dtype().clone(), array.nchunks()) .into_array() - .optimize() }) - .try_collect()?; - - Ok(Some( - unsafe { ChunkedArray::new_unchecked(new_chunks, parent.dtype().clone()) }.into_array(), - )) + }) } } diff --git a/vortex-array/src/arrays/chunked/compute/slice.rs b/vortex-array/src/arrays/chunked/compute/slice.rs index 14919301bd1..73882537140 100644 --- a/vortex-array/src/arrays/chunked/compute/slice.rs +++ b/vortex-array/src/arrays/chunked/compute/slice.rs @@ -3,7 +3,6 @@ use std::ops::Range; -use itertools::Itertools; use vortex_error::VortexResult; use crate::ArrayRef; @@ -32,7 +31,7 @@ impl SliceKernel for Chunked { // SAFETY: empty chunked array trivially satisfies all validations unsafe { return Ok(Some( - ChunkedArray::new_unchecked(vec![], array.dtype().clone()).into_array(), + ChunkedArray::new_unchecked([], array.dtype().clone()).into_array(), )); } } @@ -47,18 +46,15 @@ impl SliceKernel for Chunked { )); } - let mut chunks = (offset_chunk..length_chunk + 1) - .map(|i| array.chunk(i).clone()) - .collect_vec(); - if let Some(c) = chunks.first_mut() { - *c = c.slice(offset_in_first_chunk..c.len())?; - } - - if length_in_last_chunk == 0 { - chunks.pop(); - } else if let Some(c) = chunks.last_mut() { - *c = c.slice(0..length_in_last_chunk)?; - } + let first = array.chunk(offset_chunk); + let first = first.slice(offset_in_first_chunk..first.len())?; + let middle = (offset_chunk + 1..length_chunk).map(|i| array.chunk(i).clone()); + let last = if length_in_last_chunk == 0 { + None + } else { + Some(array.chunk(length_chunk).slice(0..length_in_last_chunk)?) + }; + let chunks = std::iter::once(first).chain(middle).chain(last); // SAFETY: chunks are slices of the original valid chunks, preserving their dtype. // All chunks maintain the same dtype as the original array. diff --git a/vortex-array/src/arrays/chunked/compute/zip.rs b/vortex-array/src/arrays/chunked/compute/zip.rs index dc43043b6c7..e177b66dd16 100644 --- a/vortex-array/src/arrays/chunked/compute/zip.rs +++ b/vortex-array/src/arrays/chunked/compute/zip.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use itertools::Itertools; use vortex_error::VortexResult; use crate::ArrayRef; @@ -30,17 +31,21 @@ impl ZipKernel for Chunked { let dtype = if_true .dtype() .union_nullability(if_false.dtype().nullability()); - let mut out_chunks = Vec::with_capacity(if_true.nchunks() + if_false.nchunks()); - - for pair in if_true.paired_chunks(&if_false) { + let chunks = if_true.paired_chunks(&if_false).map(|pair| { let pair = pair?; - let mask_slice = mask.slice(pair.pos)?; - out_chunks.push(mask_slice.zip(pair.left, pair.right)?); - } - - // SAFETY: chunks originate from zipping slices of inputs that share dtype/nullability. - let chunked = unsafe { ChunkedArray::new_unchecked(out_chunks, dtype) }; - Ok(Some(chunked.into_array())) + mask.slice(pair.pos)?.zip(pair.left, pair.right) + }); + chunks.process_results(|chunks| { + // SAFETY: chunks originate from zipping slices of inputs that share dtype/nullability. + Some(unsafe { + ChunkedArray::new_unchecked_sized( + chunks, + dtype, + if_true.nchunks() + if_false.nchunks(), + ) + .into_array() + }) + }) } } diff --git a/vortex-array/src/arrays/chunked/vtable/canonical.rs b/vortex-array/src/arrays/chunked/vtable/canonical.rs index 5d6d35d0dae..2ab0fa7b3c4 100644 --- a/vortex-array/src/arrays/chunked/vtable/canonical.rs +++ b/vortex-array/src/arrays/chunked/vtable/canonical.rs @@ -121,9 +121,6 @@ fn pack_variant_chunks( } Some(first_shredded) => { let shredded_dtype = first_shredded.dtype().clone(); - let mut shredded_chunks = Vec::with_capacity(variant_chunks.len()); - shredded_chunks.push(first_shredded.clone()); - for chunk in &variant_chunks[1..] { let shredded = chunk.shredded().ok_or_else(|| { vortex_err!( @@ -135,9 +132,14 @@ fn pack_variant_chunks( &shredded_dtype, "cannot canonicalize ChunkedArray: shredded dtype mismatch", ); - shredded_chunks.push(shredded.clone()); } + let shredded_chunks = variant_chunks.iter().map(|chunk| { + chunk + .shredded() + .vortex_expect("validated shredded presence") + .clone() + }); Some(ChunkedArray::try_new(shredded_chunks, shredded_dtype)?.into_array()) } }; @@ -170,7 +172,6 @@ fn swizzle_list_chunks( // Since each list array in `chunks` has offsets local to each array, we can reuse the existing // array's child `elements` as the chunks and recompute offsets. - let mut list_elements_chunks = Vec::with_capacity(chunks.len()); let mut num_elements = 0; // TODO(connor)[ListView]: We could potentially choose a smaller type here, but that would make @@ -184,15 +185,12 @@ fn swizzle_list_chunks( let sizes_slice_out = sizes.as_mut_slice(); let mut next_list = 0usize; - for chunk in chunks { + let element_chunks = chunks.iter().map(|chunk| -> VortexResult<_> { let chunk_array = chunk.clone().execute::(ctx)?; // By rebuilding as zero-copy to `List` and trimming all elements (to prevent gaps), we make // the final output `ListView` also zero-copyable to `List`. let chunk_array = chunk_array.rebuild(ListViewRebuildMode::MakeExact, ctx)?; - // Add the `elements` of the current array as a new chunk. - list_elements_chunks.push(chunk_array.elements().clone()); - // Cast offsets and sizes to `u64`. let offsets_arr = chunk_array .offsets() @@ -219,14 +217,15 @@ fn swizzle_list_chunks( } num_elements += chunk_array.elements().len() as u64; - } + Ok(chunk_array.elements().clone()) + }); + let chunked_elements = element_chunks.process_results(|elements| { + // SAFETY: elements come from valid ListView arrays with the same element dtype. + unsafe { ChunkedArray::new_unchecked_sized(elements, elem_dtype.clone(), chunks.len()) } + .into_array() + })?; debug_assert_eq!(next_list, len); - // SAFETY: elements are sliced from valid `ListViewArray`s (from `to_listview()`). - let chunked_elements = - unsafe { ChunkedArray::new_unchecked(list_elements_chunks, elem_dtype.clone()) } - .into_array(); - let offsets = PrimitiveArray::new(offsets.freeze(), Validity::NonNullable).into_array(); let sizes = PrimitiveArray::new(sizes.freeze(), Validity::NonNullable).into_array(); @@ -260,16 +259,18 @@ fn swizzle_fixed_size_list_chunks( ) -> VortexResult { let len: usize = chunks.iter().map(|c| c.len()).sum(); - let mut element_chunks = Vec::with_capacity(chunks.len()); - for chunk in chunks { + let element_chunks = chunks.iter().map(|chunk| -> VortexResult<_> { let chunk_array = chunk.clone().execute::(ctx)?; // A canonical `FixedSizeListArray` keeps its `elements` child trimmed to exactly // `list_size * chunk.len()` starting at the first list, so the children concatenate // cleanly into the combined `elements` array. - element_chunks.push(chunk_array.elements().clone()); - } - - let chunked_elements = ChunkedArray::try_new(element_chunks, elem_dtype.clone())?.into_array(); + Ok(chunk_array.elements().clone()) + }); + let chunked_elements = element_chunks.process_results(|elements| { + // SAFETY: every fixed-size-list chunk has the same element dtype. + unsafe { ChunkedArray::new_unchecked_sized(elements, elem_dtype.clone(), chunks.len()) } + .into_array() + })?; FixedSizeListArray::try_new(chunked_elements, list_size, validity, len) } diff --git a/vortex-array/src/arrays/constant/vtable/canonical.rs b/vortex-array/src/arrays/constant/vtable/canonical.rs index c2052999d37..65140ab4e06 100644 --- a/vortex-array/src/arrays/constant/vtable/canonical.rs +++ b/vortex-array/src/arrays/constant/vtable/canonical.rs @@ -496,10 +496,7 @@ fn tile_fixed_size_list_elements( // SAFETY: every chunk is `tile` itself, so they share its dtype and none is empty. unsafe { - ChunkedArray::new_unchecked( - std::iter::repeat_n(tile, len).collect::>(), - element_dtype.clone(), - ) + ChunkedArray::new_unchecked(std::iter::repeat_n(tile, len), element_dtype.clone()) } .into_array() } diff --git a/vortex-array/src/arrays/dict/compute/rules.rs b/vortex-array/src/arrays/dict/compute/rules.rs index 694b79cc4cc..6815e35ef05 100644 --- a/vortex-array/src/arrays/dict/compute/rules.rs +++ b/vortex-array/src/arrays/dict/compute/rules.rs @@ -64,7 +64,6 @@ impl ArrayParentReduceRule for DictionaryChunkedValuesPullUpRule { ) -> VortexResult> { let values = array.values(); let codes_dtype = array.codes().dtype().clone(); - let mut code_chunks = Vec::with_capacity(parent.nchunks()); let mut all_values_referenced = array.has_all_values_referenced(); for chunk in parent.iter_chunks() { @@ -78,9 +77,11 @@ impl ArrayParentReduceRule for DictionaryChunkedValuesPullUpRule { return Ok(None); } all_values_referenced |= dict.has_all_values_referenced(); - code_chunks.push(dict.codes().clone()); } + let code_chunks = parent + .iter_chunks() + .map(|chunk| chunk.as_::().codes().clone()); let codes = ChunkedArray::try_new(code_chunks, codes_dtype)?.into_array(); let dict = DictArray::try_new(codes, values.clone())?; let dict = if all_values_referenced { diff --git a/vortex-array/src/arrays/listview/compute/zip.rs b/vortex-array/src/arrays/listview/compute/zip.rs index a50f1909395..f414988dcad 100644 --- a/vortex-array/src/arrays/listview/compute/zip.rs +++ b/vortex-array/src/arrays/listview/compute/zip.rs @@ -6,6 +6,7 @@ use std::ops::BitAnd; use std::ops::BitOr; use std::ops::Not; +use itertools::Either; use vortex_buffer::Buffer; use vortex_buffer::BufferMut; use vortex_error::VortexExpect; @@ -75,9 +76,7 @@ impl ZipKernel for ListView { // Concatenate the two `elements` arrays without copying. If either side is already a // `ChunkedArray` (e.g. the result of a previous list-view zip), splice its chunks in // directly rather than nesting chunked arrays. - let mut chunks = Vec::with_capacity(2); - push_element_chunks(true_elements, &mut chunks); - push_element_chunks(false_elements, &mut chunks); + let chunks = element_chunks(&true_elements).chain(element_chunks(&false_elements)); let elements = ChunkedArray::try_new(chunks, result_elements_dtype)?.into_array(); let true_offsets = to_u64(if_true.offsets(), ctx)?; @@ -190,12 +189,14 @@ fn select_column( } } -/// Appends `array`'s element chunks to `chunks`, flattening a top-level [`ChunkedArray`] so the +/// Iterates over `array`'s element chunks, flattening a top-level [`ChunkedArray`] so the /// concatenated elements never nest chunked arrays. -fn push_element_chunks(array: ArrayRef, chunks: &mut Vec) { +fn element_chunks(array: &ArrayRef) -> impl Iterator + '_ { match array.as_opt::() { - Some(chunked) => chunks.extend(chunked.iter_chunks().cloned()), - None => chunks.push(array), + Some(chunked) => { + Either::Left((0..chunked.nchunks()).map(move |i| chunked.chunk(i).clone())) + } + None => Either::Right(std::iter::once(array.clone())), } } diff --git a/vortex-array/src/arrays/variant/vtable/kernel.rs b/vortex-array/src/arrays/variant/vtable/kernel.rs index 22d52ecd6a7..bf70b5c4153 100644 --- a/vortex-array/src/arrays/variant/vtable/kernel.rs +++ b/vortex-array/src/arrays/variant/vtable/kernel.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use itertools::Itertools; use vortex_error::VortexExpect; use vortex_error::VortexResult; use vortex_session::VortexSession; @@ -177,9 +178,7 @@ fn merge_typed_as_variant( let dtype = DType::Variant(Nullability::Nullable); // TODO(variant): replace this with a Variant builder once one exists. // Chunked canonicalizes to VariantArray, so this row-wise fallback is safe. - let mut chunks = Vec::with_capacity(typed.len()); - - for idx in 0..typed.len() { + let chunks = (0..typed.len()).map(|idx| -> VortexResult<_> { let typed_scalar = typed.execute_scalar(idx, ctx)?; let fallback_scalar = fallback .as_ref() @@ -187,10 +186,13 @@ fn merge_typed_as_variant( .transpose()?; let scalar = merge_typed_scalar_as_variant(typed_scalar, fallback_scalar, &dtype)?; - chunks.push(ConstantArray::new(scalar, 1).into_array()); - } - - let core_storage = ChunkedArray::try_new(chunks, dtype)?.into_array(); + Ok(ConstantArray::new(scalar, 1).into_array()) + }); + let core_storage = chunks.process_results(|chunks| { + // SAFETY: each output scalar is constructed with the requested variant dtype. + unsafe { ChunkedArray::new_unchecked_sized(chunks, dtype.clone(), typed.len()) } + .into_array() + })?; VariantArray::try_new(core_storage, None).map(|array| array.into_array()) } diff --git a/vortex-array/src/builders/child.rs b/vortex-array/src/builders/child.rs index 923b7f76114..6138c7a47f0 100644 --- a/vortex-array/src/builders/child.rs +++ b/vortex-array/src/builders/child.rs @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors use vortex_buffer::BufferAllocatorRef; +use vortex_error::VortexExpect; use vortex_error::VortexResult; use vortex_error::vortex_ensure_eq; @@ -133,12 +134,13 @@ impl ChildBuilder { self.flush_pending(); self.chunks_len = 0; - let mut chunks = std::mem::take(&mut self.chunks); - if chunks.len() == 1 { - return chunks.remove(0); + if self.chunks.len() == 1 { + return self.chunks.pop().vortex_expect("single chunk"); } - unsafe { ChunkedArray::new_unchecked(chunks, self.dtype.clone()) }.into_array() + // SAFETY: every accumulated chunk has the builder's dtype. + unsafe { ChunkedArray::new_unchecked(self.chunks.drain(..), self.dtype.clone()) } + .into_array() } /// Moves whatever the scalar builder holds into `chunks`, keeping the chunks in logical order. diff --git a/vortex-array/src/builders/fixed_size_list.rs b/vortex-array/src/builders/fixed_size_list.rs index 66a4738cfc6..f909f116881 100644 --- a/vortex-array/src/builders/fixed_size_list.rs +++ b/vortex-array/src/builders/fixed_size_list.rs @@ -156,7 +156,7 @@ impl FixedSizeListBuilder { // SAFETY: every chunk is `array` itself, so they share its dtype and none is empty. let tiled = unsafe { ChunkedArray::new_unchecked( - std::iter::repeat_n(array.clone(), n).collect::>(), + std::iter::repeat_n(array.clone(), n), self.element_dtype().clone(), ) }; diff --git a/vortex-array/src/iter.rs b/vortex-array/src/iter.rs index b4cd01d875d..d23060527ec 100644 --- a/vortex-array/src/iter.rs +++ b/vortex-array/src/iter.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use itertools::Itertools; use vortex_error::VortexResult; +use vortex_error::vortex_ensure_eq; use crate::ArrayRef; use crate::IntoArray; @@ -53,6 +54,10 @@ where fn next(&mut self) -> Option { self.inner.next() } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } } impl ArrayIterator for ArrayIteratorAdapter @@ -81,12 +86,26 @@ pub trait ArrayIteratorExt: ArrayIterator { Self: Sized, { let dtype = self.dtype().clone(); - let mut chunks: Vec = self.try_collect()?; - if chunks.len() == 1 { - Ok(chunks.remove(0)) - } else { - Ok(ChunkedArray::try_new(chunks, dtype)?.into_array()) + let mut chunks = self.peekable(); + let Some(first) = chunks.next().transpose()? else { + return Ok(ChunkedArray::try_new([], dtype)?.into_array()); + }; + if chunks.peek().is_none() { + return Ok(first); } + let chunks = std::iter::once(Ok(first)) + .chain(chunks) + .map(|chunk| -> VortexResult<_> { + let chunk = chunk?; + vortex_ensure_eq!(chunk.dtype(), &dtype); + Ok(chunk) + }); + let expected_nchunks = chunks.size_hint().0; + chunks.process_results(|chunks| { + // SAFETY: the iterator validates the dtype of every successfully yielded chunk. + unsafe { ChunkedArray::new_unchecked_sized(chunks, dtype.clone(), expected_nchunks) } + .into_array() + }) } } diff --git a/vortex-array/src/scalar_fn/fns/variant_get/mod.rs b/vortex-array/src/scalar_fn/fns/variant_get/mod.rs index ef493d0146e..35de4b729bd 100644 --- a/vortex-array/src/scalar_fn/fns/variant_get/mod.rs +++ b/vortex-array/src/scalar_fn/fns/variant_get/mod.rs @@ -5,6 +5,7 @@ use std::fmt; use std::fmt::Display; use std::fmt::Formatter; +use itertools::Itertools; use prost::Message; use vortex_error::VortexResult; use vortex_error::vortex_ensure; @@ -156,15 +157,16 @@ impl ScalarFnVTable for VariantGet { // TODO(variant): replace this with a Variant builder once one exists. // Chunked canonicalizes to VariantArray, so this row-wise fallback is safe. - let mut chunks = Vec::with_capacity(input.len()); - - for idx in 0..input.len() { + let chunks = (0..input.len()).map(|idx| -> VortexResult<_> { let scalar = input.execute_scalar(idx, ctx)?; let output = variant_get_scalar(&scalar, options, &dtype)?; - chunks.push(ConstantArray::new(output, 1).into_array()); - } - - let array = ChunkedArray::try_new(chunks, dtype)?.into_array(); + Ok(ConstantArray::new(output, 1).into_array()) + }); + let array = chunks.process_results(|chunks| { + // SAFETY: each output scalar is constructed with the requested variant dtype. + unsafe { ChunkedArray::new_unchecked_sized(chunks, dtype.clone(), input.len()) } + .into_array() + })?; VariantArray::try_new(array, None).map(|array| array.into_array()) } diff --git a/vortex-bench/src/conversions.rs b/vortex-bench/src/conversions.rs index d48356d4fa1..096364bf8d2 100644 --- a/vortex-bench/src/conversions.rs +++ b/vortex-bench/src/conversions.rs @@ -10,6 +10,7 @@ use arrow_array::RecordBatch; use arrow_select::concat::concat_batches; use futures::StreamExt; use futures::TryStreamExt; +use itertools::Itertools; use parquet::arrow::AsyncArrowWriter; use parquet::arrow::ParquetRecordBatchStreamBuilder; use parquet::arrow::arrow_reader::ArrowReaderMetadata; @@ -144,13 +145,16 @@ pub async fn parquet_to_vortex_chunks_with_batch_size( .ok_or_else(|| anyhow::anyhow!("cannot convert an empty Parquet file"))?; let combined = concat_batches(&schema, &batches)?; - let mut chunks = Vec::with_capacity(combined.num_rows().div_ceil(batch_size)); - for start in (0..combined.num_rows()).step_by(batch_size) { + let dtype = SESSION.arrow().from_arrow_schema(schema.as_ref())?; + let chunks = (0..combined.num_rows()).step_by(batch_size).map(|start| { let len = batch_size.min(combined.num_rows() - start); - chunks.push(record_batch_to_vortex(combined.slice(start, len))?); - } - - Ok(ChunkedArray::from_iter(chunks)) + record_batch_to_vortex(combined.slice(start, len)) + }); + let expected_nchunks = combined.num_rows().div_ceil(batch_size); + Ok(chunks.process_results(|chunks| { + // SAFETY: every batch is a slice of `combined` and is converted using the same schema. + unsafe { ChunkedArray::new_unchecked_sized(chunks, dtype, expected_nchunks) } + })?) } /// Convert one Arrow [`RecordBatch`] into a canonical Vortex array. diff --git a/vortex-bench/src/vector_dataset/convert.rs b/vortex-bench/src/vector_dataset/convert.rs index 7f21d8c1596..1b7c9a45ea5 100644 --- a/vortex-bench/src/vector_dataset/convert.rs +++ b/vortex-bench/src/vector_dataset/convert.rs @@ -3,6 +3,7 @@ // TODO(connor): Should we re-export this through `conversions.rs`? +use itertools::Itertools; use vortex::array::ArrayRef; use vortex::array::EmptyMetadata; use vortex::array::IntoArray; @@ -24,6 +25,7 @@ use vortex::dtype::extension::ExtDType; use vortex::error::VortexExpect; use vortex::error::VortexResult; use vortex::error::vortex_bail; +use vortex::error::vortex_ensure_eq; use vortex::error::vortex_err; use vortex_tensor::vector::Vector; @@ -62,17 +64,27 @@ use crate::SESSION; /// - The input has zero rows (the dimension cannot be inferred from empty input). pub fn list_to_vector_ext(input: ArrayRef) -> VortexResult { if let Some(chunked) = input.as_opt::() { - let converted: Vec = chunked + let mut converted = chunked .iter_chunks() - .map(|chunk| list_to_vector_ext(chunk.clone())) - .collect::>()?; + .map(|chunk| list_to_vector_ext(chunk.clone())); - let Some(first) = converted.first() else { + let Some(first) = converted.next().transpose()? else { vortex_bail!("list_to_vector_ext: chunked input has no chunks"); }; let dtype = first.dtype().clone(); - return Ok(ChunkedArray::try_new(converted, dtype)?.into_array()); + let chunks = std::iter::once(Ok(first)) + .chain(converted) + .map(|chunk| -> VortexResult<_> { + let chunk = chunk?; + vortex_ensure_eq!(chunk.dtype(), &dtype); + Ok(chunk) + }); + return chunks.process_results(|chunks| { + // SAFETY: the iterator checks that all chunks have the same vector dtype and dimension. + unsafe { ChunkedArray::new_unchecked_sized(chunks, dtype.clone(), chunked.nchunks()) } + .into_array() + }); } // `parquet_to_vortex_chunks` produces `ListView` arrays for list columns by default; diff --git a/vortex-compressor/src/compressor/sample.rs b/vortex-compressor/src/compressor/sample.rs index 4f44dd330fc..dcdce0dec1f 100644 --- a/vortex-compressor/src/compressor/sample.rs +++ b/vortex-compressor/src/compressor/sample.rs @@ -51,14 +51,11 @@ pub(crate) fn sample(input: &ArrayRef, sample_size: u32, sample_count: u32) -> A ); // For every slice, grab the relevant slice and repack into a new PrimitiveArray. - let chunks: Vec<_> = slices - .into_iter() - .map(|(start, end)| { - input - .slice(start..end) - .vortex_expect("slice should succeed") - }) - .collect(); + let chunks = slices.into_iter().map(|(start, end)| { + input + .slice(start..end) + .vortex_expect("slice should succeed") + }); // SAFETY: all chunks are slices of `input`, so they share its dtype. unsafe { ChunkedArray::new_unchecked(chunks, input.dtype().clone()) }.into_array() } diff --git a/vortex-file/benches/split_collection.rs b/vortex-file/benches/split_collection.rs index 8985f26e833..3a303be37d4 100644 --- a/vortex-file/benches/split_collection.rs +++ b/vortex-file/benches/split_collection.rs @@ -81,20 +81,17 @@ static SESSION: LazyLock = LazyLock::new(|| { fn make_file(columns: usize, chunks: usize) -> VortexFile { let field_names = (0..columns).map(|c| format!("col_{c}")).collect::>(); - let struct_chunks = (0..chunks) - .map(|chunk| { - let fields = field_names - .iter() - .map(|name| { - let start = (chunk * ROWS_PER_CHUNK) as i64; - let values = - Buffer::from_iter(start..start + ROWS_PER_CHUNK as i64).into_array(); - (name.as_str(), values) - }) - .collect::>(); - StructArray::from_fields(&fields).unwrap().into_array() - }) - .collect::>(); + let struct_chunks = (0..chunks).map(|chunk| { + let fields = field_names + .iter() + .map(|name| { + let start = (chunk * ROWS_PER_CHUNK) as i64; + let values = Buffer::from_iter(start..start + ROWS_PER_CHUNK as i64).into_array(); + (name.as_str(), values) + }) + .collect::>(); + StructArray::from_fields(&fields).unwrap().into_array() + }); let array = ChunkedArray::from_iter(struct_chunks).into_array(); let strategy = vortex_file::WriteStrategyBuilder::from_session(&SESSION) diff --git a/vortex-python/src/arrays/from_arrow.rs b/vortex-python/src/arrays/from_arrow.rs index 92cae1ad55a..f3855eedbcf 100644 --- a/vortex-python/src/arrays/from_arrow.rs +++ b/vortex-python/src/arrays/from_arrow.rs @@ -7,13 +7,15 @@ use arrow_array::make_array; use arrow_data::ArrayData as ArrowArrayData; use arrow_schema::DataType; use arrow_schema::Field; +use itertools::Itertools; use pyo3::exceptions::PyValueError; use pyo3::intern; use pyo3::prelude::*; use vortex::array::IntoArray; use vortex::array::arrays::ChunkedArray; use vortex::error::VortexError; -use vortex::error::VortexResult; +use vortex::error::vortex_ensure_eq; +use vortex::error::vortex_err; use vortex_arrow::ArrowSessionExt; use crate::arrays::PyArrayRef; @@ -51,15 +53,6 @@ pub(super) fn from_arrow(obj: &Borrowed<'_, '_, PyAny>) -> PyVortexResult>>()?; // One nullability for the whole array so every chunk shares a dtype. let is_nullable = arrow_chunks.iter().any(|a| a.is_nullable()); - let encoded_chunks = arrow_chunks - .into_iter() - .map(|arrow_array| { - session() - .arrow() - .from_arrow_array(arrow_array, is_nullable) - .map_err(PyVortexError::from) - }) - .collect::>>()?; let arrow_dtype = obj .getattr(intern!(py, "type")) .and_then(|v| DataType::from_pyarrow(&v.as_borrowed()))?; @@ -67,27 +60,49 @@ pub(super) fn from_arrow(obj: &Borrowed<'_, '_, PyAny>) -> PyVortexResult PyVortexResult<_> { + let encoded = session() + .arrow() + .from_arrow_array(arrow_array, is_nullable) + .map_err(PyVortexError::from)?; + if encoded.dtype() != &dtype { + return Err(vortex_err!( + "expected type: {} but instead got {}", + &dtype, + encoded.dtype() + ) + .into()); + } + Ok(encoded) + }); + let expected_nchunks = encoded_chunks.size_hint().0; + let array = encoded_chunks.process_results(|chunks| { + // SAFETY: the iterator validates each converted chunk against the declared dtype. + unsafe { ChunkedArray::new_unchecked_sized(chunks, dtype.clone(), expected_nchunks) } + })?; + Ok(PyArrayRef::from(array.into_array())) } else if obj.is_instance(table)? { let array_stream = ArrowArrayStreamReader::from_pyarrow(&obj.as_borrowed())?; let dtype = session() .arrow() .from_arrow_schema(array_stream.schema().as_ref()) .map_err(|e| PyValueError::new_err(e.to_string()))?; - let chunks = array_stream - .into_iter() - .map(|b| { - b.map_err(VortexError::from).and_then(|b| { - let schema = b.schema(); - session().arrow().from_arrow_record_batch(b, &schema) - }) + let encoded_chunks = array_stream.into_iter().map(|b| { + b.map_err(VortexError::from).and_then(|b| { + let schema = b.schema(); + let encoded = session().arrow().from_arrow_record_batch(b, &schema)?; + vortex_ensure_eq!(encoded.dtype(), &dtype); + Ok(encoded) }) - .collect::>>()?; - Ok(PyArrayRef::from( - ChunkedArray::try_new(chunks, dtype)?.into_array(), - )) + }); + let expected_nchunks = encoded_chunks.size_hint().0; + let array = encoded_chunks.process_results(|chunks| { + // SAFETY: the iterator validates each converted batch against the stream's dtype. + unsafe { ChunkedArray::new_unchecked_sized(chunks, dtype.clone(), expected_nchunks) } + })?; + Ok(PyArrayRef::from(array.into_array())) } else { Err(PyValueError::new_err("Cannot convert object to Vortex array").into()) } From 141abdc590aa990c5e7b6c49dbc6832c254d50d4 Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Wed, 4 Nov 2026 16:39:13 +0000 Subject: [PATCH 23/41] perf(fastlanes): push FoR ordering comparisons to the encoded values (#10147) FoR only compared for equality, so ordering comparisons decoded the array, which also bypassed the bit-packed child's own compare kernel. Encoding subtracts the minimum, so the encoded values are non-negative distances. For unsigned types the kernel compares them against the constant's distance directly; for signed types it does so when the child is bit-packed, which keeps the distances non-negative in the signed reading, and otherwise falls back to decoding. A distance that does not fit the type answers by which side of the reference the constant lies on. Signed-off-by: Robert Kruszewski --- encodings/fastlanes/src/for_/array/mod.rs | 3 + .../fastlanes/src/for_/compute/compare.rs | 326 ++++++++++++++++-- 2 files changed, 295 insertions(+), 34 deletions(-) diff --git a/encodings/fastlanes/src/for_/array/mod.rs b/encodings/fastlanes/src/for_/array/mod.rs index 21f4bf2e94f..9310fd7e46b 100644 --- a/encodings/fastlanes/src/for_/array/mod.rs +++ b/encodings/fastlanes/src/for_/array/mod.rs @@ -39,6 +39,9 @@ pub struct FoRSlots { /// references, it decodes as `encoded[i] + blocked_references[(offset + i) / FL_CHUNK_SIZE]`, /// where `offset` is the position of the first element within the first chunk. Both use wrapping /// arithmetic. +/// +/// Each reference must be less than or equal to all valid values that decode against it. Thus, +/// each encoded value is the exact unsigned distance from its reference to the decoded value. #[derive(Clone, Debug)] pub struct FoRData { pub(super) offset: u16, diff --git a/encodings/fastlanes/src/for_/compute/compare.rs b/encodings/fastlanes/src/for_/compute/compare.rs index d9dfcc6d9f8..4294e9b93dd 100644 --- a/encodings/fastlanes/src/for_/compute/compare.rs +++ b/encodings/fastlanes/src/for_/compute/compare.rs @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use std::ops::Shr; - use num_traits::WrappingSub; use vortex_array::ArrayRef; use vortex_array::ArrayView; @@ -10,14 +8,18 @@ use vortex_array::ExecutionCtx; use vortex_array::IntoArray; use vortex_array::arrays::ConstantArray; use vortex_array::builtins::ArrayBuiltins; -use vortex_array::dtype::NativePType; +use vortex_array::dtype::DType; +use vortex_array::dtype::IntegerPType; use vortex_array::dtype::Nullability; use vortex_array::match_each_integer_ptype; use vortex_array::scalar::PValue; use vortex_array::scalar::Scalar; +use vortex_array::scalar_fn::fns::between::BetweenOptions; +use vortex_array::scalar_fn::fns::between::StrictComparison; use vortex_array::scalar_fn::fns::binary::CompareKernel; use vortex_array::scalar_fn::fns::operators::CompareOperator; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_array::validity::Validity; use vortex_error::VortexError; use vortex_error::VortexExpect as _; use vortex_error::VortexResult; @@ -36,6 +38,7 @@ impl CompareKernel for FoR { ) -> VortexResult> { if let Some(constant) = rhs.as_constant() && let Some(constant) = constant.as_primitive_opt() + && constant.ptype() == lhs.dtype().as_ptype() { match_each_integer_ptype!(constant.ptype(), |T| { return compare_constant( @@ -53,62 +56,154 @@ impl CompareKernel for FoR { } } +/// Compares the array to a constant through its encoded values. +/// +/// No valid value is below the reference, so a constant below it answers every row. Otherwise +/// the constant is a distance from the reference. Equality survives the wrapping subtraction. +/// Ordering compares unsigned distances. fn compare_constant( lhs: ArrayView<'_, FoR>, - mut rhs: T, + rhs: T, nullability: Nullability, operator: CompareOperator, ) -> VortexResult> where - T: NativePType + WrappingSub + Shr, + T: IntegerPType + WrappingSub, T: TryFrom, PValue: From, { - // For now, we only support equals and not equals. Comparisons are a little more fiddly to - // get right regarding how to handle overflow and the wrapping subtraction. - if !matches!(operator, CompareOperator::Eq | CompareOperator::NotEq) { - return Ok(None); - } - // TODO(mk): support many references. - let FoRReferences::Global(reference) = lhs.references() else { return Ok(None); }; - let reference = reference.as_primitive().typed_value::(); + let reference = reference + .as_primitive() + .typed_value::() + .vortex_expect("FoR reference is non-null and matches the array ptype"); - // We encode the RHS into the FoR domain. - if let Some(reference) = reference { - rhs = rhs.wrapping_sub(&reference); + if rhs < reference { + let answer = matches!( + operator, + CompareOperator::NotEq | CompareOperator::Gt | CompareOperator::Gte + ); + return constant_answer(lhs, answer, nullability).map(Some); } - // Wrap up the RHS into a scalar and cast to the encoded DType (this will be the equivalent - // unsigned integer type). - let rhs = Scalar::primitive(rhs, nullability); + let distance = rhs.wrapping_sub(&reference); + let is_equality = matches!(operator, CompareOperator::Eq | CompareOperator::NotEq); - lhs.encoded() - .binary( - ConstantArray::new(rhs, lhs.len()).into_array(), - Operator::from(operator), - ) - .map(Some) + if is_equality || T::PTYPE.is_unsigned_int() { + compare_unsigned_encoded(lhs, distance, nullability, operator).map(Some) + } else { + compare_signed_encoded(lhs, distance, nullability, operator).map(Some) + } +} + +/// Compares the encoded values to `distance` with the operator unchanged. +/// +/// This is correct for equality on any integer type, and for ordering on unsigned types, where +/// the encoded values and `distance` order as unsigned distances from the reference. +fn compare_unsigned_encoded>( + lhs: ArrayView<'_, FoR>, + distance: T, + nullability: Nullability, + operator: CompareOperator, +) -> VortexResult { + lhs.encoded().binary( + ConstantArray::new(Scalar::primitive(distance, nullability), lhs.len()).into_array(), + Operator::from(operator), + ) +} + +/// Orders signed encoded values as unsigned distances. +/// +/// Distances above `T::MAX` read as negative, so each operator is one signed range or its +/// complement: `[0, distance)` for a non-negative `distance`, `[distance, -1]` otherwise. +fn compare_signed_encoded( + lhs: ArrayView<'_, FoR>, + distance: T, + nullability: Nullability, + operator: CompareOperator, +) -> VortexResult +where + T: IntegerPType + Into, +{ + let strictness = |strict: bool| { + if strict { + StrictComparison::Strict + } else { + StrictComparison::NonStrict + } + }; + + let below = matches!(operator, CompareOperator::Lt | CompareOperator::Lte); + let (lower, upper, options, negate) = if distance >= T::zero() { + let upper_strict = matches!(operator, CompareOperator::Lt | CompareOperator::Gte); + let options = BetweenOptions { + lower_strict: StrictComparison::NonStrict, + upper_strict: strictness(upper_strict), + }; + (T::zero(), distance, options, !below) + } else { + let lower_strict = matches!(operator, CompareOperator::Lte | CompareOperator::Gt); + let options = BetweenOptions { + lower_strict: strictness(lower_strict), + upper_strict: StrictComparison::NonStrict, + }; + (distance, !T::zero(), options, below) + }; + + let bound = |value: T| { + ConstantArray::new(Scalar::primitive(value, nullability), lhs.len()).into_array() + }; + let in_range = lhs + .encoded() + .clone() + .between(bound(lower), bound(upper), options)?; + + if negate { in_range.not() } else { Ok(in_range) } +} + +/// A constant answer for every row that still carries the array's validity. +fn constant_answer( + lhs: ArrayView<'_, FoR>, + value: bool, + rhs_nullability: Nullability, +) -> VortexResult { + let len = lhs.len(); + let nullability = lhs.dtype().nullability() | rhs_nullability; + + Ok(match lhs.validity()?.union_nullability(nullability) { + Validity::NonNullable | Validity::AllValid => { + ConstantArray::new(Scalar::bool(value, nullability), len).into_array() + } + Validity::AllInvalid => { + ConstantArray::new(Scalar::null(DType::Bool(nullability)), len).into_array() + } + Validity::Array(validity) => ConstantArray::new(Scalar::bool(value, nullability), len) + .into_array() + .mask(validity)?, + }) } #[cfg(test)] mod tests { use std::sync::LazyLock; + use rstest::rstest; use vortex_array::IntoArray; use vortex_array::VortexSessionExecute; use vortex_array::arrays::BoolArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::assert_arrays_eq; use vortex_array::dtype::DType; + use vortex_array::dtype::NativePType; use vortex_array::validity::Validity; use vortex_buffer::buffer; use vortex_session::VortexSession; use super::*; + use crate::BitPackedData; use crate::FoR; use crate::FoRArray; @@ -159,16 +254,19 @@ mod tests { &mut SESSION.create_execution_ctx() ); - for op in [ - CompareOperator::Lt, - CompareOperator::Lte, - CompareOperator::Gt, - CompareOperator::Gte, + for (op, expected) in [ + (CompareOperator::Lt, [true, false, true]), + (CompareOperator::Lte, [true, true, true]), + (CompareOperator::Gt, [false, false, false]), + (CompareOperator::Gte, [false, true, false]), ] { - assert!( - compare_constant(lhs.as_view(), 30i32, Nullability::NonNullable, op) - .unwrap() - .is_none() + let result = compare_constant(lhs.as_view(), 30i32, Nullability::NonNullable, op) + .unwrap() + .unwrap(); + assert_arrays_eq!( + result, + BoolArray::from_iter(expected.map(Some)), + &mut SESSION.create_execution_ctx() ); } } @@ -208,6 +306,39 @@ mod tests { ); } + /// A constant below the reference answers every row, and a nullable array keeps the result + /// nullable even when the constant is not. + #[rstest] + #[case::all_valid(Validity::AllValid, [Some(true); 3])] + #[case::all_invalid(Validity::AllInvalid, [None; 3])] + #[case::array(Validity::from_iter([true, false, true]), [Some(true), None, Some(true)])] + fn compare_below_reference_nullable_array( + #[case] validity: Validity, + #[case] expected: [Option; 3], + ) -> VortexResult<()> { + let lhs = for_arr( + PrimitiveArray::new(buffer!(0i32, 20, 2), validity).into_array(), + Scalar::from(10), + ); + + let result = compare_constant( + lhs.as_view(), + 5i32, + Nullability::NonNullable, + CompareOperator::Gt, + )? + .vortex_expect("constant below the reference has an answer"); + + assert_eq!(result.dtype(), &DType::Bool(Nullability::Nullable)); + assert_arrays_eq!( + result, + BoolArray::from_iter(expected), + &mut SESSION.create_execution_ctx() + ); + + Ok(()) + } + #[test] fn compare_non_encodable_constant() { let reference = Scalar::from(10); @@ -286,4 +417,131 @@ mod tests { &mut SESSION.create_execution_ctx() ); } + + const ALL_OPERATORS: [CompareOperator; 6] = [ + CompareOperator::Eq, + CompareOperator::NotEq, + CompareOperator::Lt, + CompareOperator::Lte, + CompareOperator::Gt, + CompareOperator::Gte, + ]; + + /// Encodes the values against their minimum. + fn encode_with_min( + values: &PrimitiveArray, + ctx: &mut ExecutionCtx, + ) -> VortexResult<(T, PrimitiveArray)> + where + T: NativePType + WrappingSub, + T: for<'a> TryFrom<&'a Scalar, Error = VortexError>, + { + let reference = values + .clone() + .into_array() + .statistics() + .compute_min::(ctx) + .vortex_expect("min"); + let encoded = PrimitiveArray::new( + values + .as_slice::() + .iter() + .map(|v| v.wrapping_sub(&reference)) + .collect::>(), + values.validity()?, + ); + Ok((reference, encoded)) + } + + /// Asserts the kernel engages and matches the primitive comparison. + fn assert_matches_primitive>( + array: &FoRArray, + values: &PrimitiveArray, + constants: impl IntoIterator, + ctx: &mut ExecutionCtx, + ) -> VortexResult<()> { + for constant in constants { + let rhs = ConstantArray::new(constant, values.len()).into_array(); + for operator in ALL_OPERATORS { + let actual = ::compare(array.as_view(), &rhs, operator, ctx)? + .unwrap_or_else(|| panic!("FoR compare must engage for {operator:?}")); + let expected = values + .clone() + .into_array() + .binary(rhs.clone(), Operator::from(operator))?; + assert_arrays_eq!(actual, expected, ctx); + } + } + Ok(()) + } + + /// Asserts FoR over a bit-packed child matches the primitive comparison. + fn assert_bit_packed_matches_primitive( + values: PrimitiveArray, + bit_width: u8, + constants: impl IntoIterator, + ) -> VortexResult<()> + where + T: NativePType + WrappingSub + Into + Into, + T: for<'a> TryFrom<&'a Scalar, Error = VortexError>, + { + let mut ctx = SESSION.create_execution_ctx(); + let (reference, encoded) = encode_with_min::(&values, &mut ctx)?; + let packed = BitPackedData::encode(&encoded.into_array(), bit_width, &mut ctx)?; + let array = FoR::try_new(packed.into_array(), reference.into())?; + + assert_matches_primitive(&array, &values, constants, &mut ctx) + } + + /// Distances above `i8::MAX` read as negative encoded values. + #[test] + fn signed_distances_above_max_match_primitive() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let values = PrimitiveArray::from_option_iter( + (i8::MIN..=i8::MAX).map(|v| (v % 7 != 3).then_some(v)), + ); + let (reference, encoded) = encode_with_min::(&values, &mut ctx)?; + let array = FoR::try_new(encoded.into_array(), reference.into())?; + + assert_matches_primitive(&array, &values, i8::MIN..=i8::MAX, &mut ctx) + } + + #[test] + fn signed_bit_packed_matches_primitive() -> VortexResult<()> { + let values = PrimitiveArray::from_iter((0..2000i32).map(|i| { + if i % 97 == 0 { + -1_000 + i * 50 + } else { + -1_000 + i % 40 + } + })); + let constants = [ + i32::MIN, + -1_001, + -1_000, + -999, + -980, + 0, + 98_900, + 98_901, + i32::MAX, + ]; + assert_bit_packed_matches_primitive(values, 6, constants) + } + + #[test] + fn unsigned_bit_packed_matches_primitive() -> VortexResult<()> { + let values = PrimitiveArray::from_iter((0..1500u16).map(|i| 1_000 + i % 70)); + let constants = [0u16, 999, 1_000, 1_001, 1_035, 1_069, 1_070, u16::MAX]; + assert_bit_packed_matches_primitive(values, 7, constants) + } + + #[test] + fn nullable_bit_packed_matches_primitive() -> VortexResult<()> { + let values = PrimitiveArray::from_option_iter( + (0..1200i64).map(|i| (i % 5 != 0).then_some(-50 + i % 30)), + ); + let constants = [i64::MIN, -51, -50, -40, -21, -20, i64::MAX]; + assert_bit_packed_matches_primitive(values, 5, constants) + } } From bf3cfd079917af6ac6d27a2baf07cded1f63e958 Mon Sep 17 00:00:00 2001 From: Rasheed <99349713+gitRasheed@users.noreply.github.com> Date: Wed, 4 Nov 2026 16:44:15 +0000 Subject: [PATCH 24/41] perf(spark): split large files into row ranges (#11118) Both read paths return `false` from `isSplitable`, so every Vortex file is one Spark input partition and a large file is read by one task. A dataset of a few large files can't use more tasks than it has files. - `isSplitable` is true for `.vortex` files on V1 and V2, so Spark plans byte splits as it does for Parquet, sized by `spark.sql.files.maxPartitionBytes`. - Each reader maps its byte range to a row range: an offset maps to the row at the same fraction of the file, rounded down to 8192 rows, the default writer's block. Neighbouring splits share an offset, so they share a row boundary and every row is read once whatever offsets Spark picks. The row count comes from the footer the reader opens anyway. Without an exact row count, the first split reads the whole file and the others read nothing. - A pushed `COUNT(*)` is not split, since it reads only the footer, and scan statistics read each split file's footer once. - On by default; `vortex.splitFiles=false` turns it off. A footer is read again on executors that haven't cached it, and a chunk that spans a cut is decoded by both splits next to it. Signed-off-by: gitRasheed <99349713+gitRasheed@users.noreply.github.com> --- docs/user-guide/spark.md | 5 + .../java/dev/vortex/spark/read/SplitRows.java | 38 +++ .../spark/read/VortexPartitionReader.java | 22 +- .../dev/vortex/spark/VortexFileFormat.scala | 8 +- .../dev/vortex/spark/read/VortexScan.scala | 19 +- .../dev/vortex/spark/VortexFileSplitTest.java | 240 ++++++++++++++++++ .../dev/vortex/spark/read/SplitRowsTest.java | 70 +++++ 7 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 java/vortex-spark/common/src/main/java/dev/vortex/spark/read/SplitRows.java create mode 100644 java/vortex-spark/common/src/test/java/dev/vortex/spark/VortexFileSplitTest.java create mode 100644 java/vortex-spark/common/src/test/java/dev/vortex/spark/read/SplitRowsTest.java diff --git a/docs/user-guide/spark.md b/docs/user-guide/spark.md index 1742f2cf1aa..8bd797cba0a 100644 --- a/docs/user-guide/spark.md +++ b/docs/user-guide/spark.md @@ -199,6 +199,7 @@ own storage clients are not used by the Spark connector. | `vortex.workerThreads` | `4` | Background threads driving Vortex futures. JVM-wide, and set once per JVM by the first read or write. | | `vortex.session.provider` | — | Fully-qualified name of a `VortexSessionProvider` supplying a custom session on the driver and on every executor. | | `vortex.aggregatePushdown` | `true` | Answer `COUNT(*)` from file footers. | +| `vortex.splitFiles` | `true` | Let Spark split a large file across tasks, as it does Parquet files. | | `vortex.mergeSchema` | `true` | Merge every file's footer schema, rather than reading one footer. | | `vortex.stats.rowCount` | `true` | Read footer row counts for Spark statistics. | | `vortex.footerParallelism` | `8` | Maximum concurrent footer reads, for schema merging and statistics. | @@ -211,6 +212,10 @@ keeps planning bounded on a large dataset by reporting no row count instead. Rai row counts matter more than planning time, or set `vortex.stats.rowCount` to `false` to stop reading footers at all. +Each task of a split file reads the rows at the same fraction of the file as its byte range, cut +on 8192-row boundaries. A footer is read again on executors that haven't cached it, and a chunk +that spans a cut is decoded by both tasks next to it. + ## Supported Types | Spark Type | Vortex Type | diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/SplitRows.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/SplitRows.java new file mode 100644 index 00000000000..0dea77216f7 --- /dev/null +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/SplitRows.java @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +package dev.vortex.spark.read; + +import java.math.BigInteger; + +/** + * The rows that one byte split of a Vortex file reads. Each offset maps to the row at the same fraction of the file, so + * neighbouring splits share a row boundary and every row is read once whatever offsets Spark picks. + */ +record SplitRows(long begin, long end) { + // The default writer builds chunks from whole blocks of this many rows, so cuts land on block boundaries. + static final long ROW_BLOCK = 8192; + + /** The rows that bytes {@code [start, start + length)} of a {@code fileSize}-byte file read. */ + static SplitRows of(long start, long length, long fileSize, long rows) { + return new SplitRows(rowAt(start, fileSize, rows), rowAt(start + length, fileSize, rows)); + } + + long count() { + return end - begin; + } + + private static long rowAt(long offset, long fileSize, long rows) { + if (offset >= fileSize) { + return rows; + } + if (offset <= 0) { + return 0; + } + long row = BigInteger.valueOf(offset) + .multiply(BigInteger.valueOf(rows)) + .divide(BigInteger.valueOf(fileSize)) + .longValueExact(); + return row / ROW_BLOCK * ROW_BLOCK; + } +} diff --git a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexPartitionReader.java b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexPartitionReader.java index 7a2b6afa6b7..94189713274 100644 --- a/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexPartitionReader.java +++ b/java/vortex-spark/common/src/main/java/dev/vortex/spark/read/VortexPartitionReader.java @@ -4,6 +4,7 @@ package dev.vortex.spark.read; import dev.vortex.api.DataSource; +import dev.vortex.api.DataSource.RowCount; import dev.vortex.api.Expression; import dev.vortex.api.Partition; import dev.vortex.api.Scan; @@ -91,13 +92,32 @@ public VortexPartitionReader( // scan anyway, so one that reads a column this file lacks is dropped rather than pushed. buildFilterExpression(pushedFilters, restrictTo(dataSchema, fileColumns)) .ifPresent(options::filter); - scan = dataSource.scan(options.build()); + Optional rows = splitRows(file, dataSource.rowCount()); + if (rows.isPresent() && rows.get().count() == 0) { + // The native scan reads a range of 0 to 0 as the whole file. + exhausted = true; + } else { + rows.ifPresent(range -> options.rowRangeBegin(range.begin()).rowRangeEnd(range.end())); + scan = dataSource.scan(options.build()); + } } catch (RuntimeException e) { closeReadableAfterFailure(e); throw e; } } + /** The rows this split of {@code file} reads, or empty when it reads them all. */ + private static Optional splitRows(PartitionedFile file, RowCount rowCount) { + if (file.start() == 0 && file.length() >= file.fileSize()) { + return Optional.empty(); + } + if (!(rowCount instanceof RowCount.Exact exact)) { + // Without an exact count no two splits can agree on a boundary, so the first reads everything. + return file.start() == 0 ? Optional.empty() : Optional.of(new SplitRows(0, 0)); + } + return Optional.of(SplitRows.of(file.start(), file.length(), file.fileSize(), exact.value())); + } + private static StructType restrictTo(StructType schema, List fileColumns) { // Native filters use physical field names, at every level. Filters on other paths stay with Spark. Map byName = new HashMap<>(); diff --git a/java/vortex-spark/common/src/main/scala/dev/vortex/spark/VortexFileFormat.scala b/java/vortex-spark/common/src/main/scala/dev/vortex/spark/VortexFileFormat.scala index 0ca50c839ec..c85b333a337 100644 --- a/java/vortex-spark/common/src/main/scala/dev/vortex/spark/VortexFileFormat.scala +++ b/java/vortex-spark/common/src/main/scala/dev/vortex/spark/VortexFileFormat.scala @@ -25,7 +25,11 @@ import org.apache.spark.sql.vectorized.{ColumnVector, ColumnarBatch} import dev.vortex.spark.VortexOptions import dev.vortex.spark.io.{VortexFile, VortexIo} -import dev.vortex.spark.read.{VortexFooterReader, VortexPartitionReader} +import dev.vortex.spark.read.{ + VortexFooterReader, + VortexPartitionReader, + VortexScan +} import dev.vortex.spark.write.{SparkToArrowSchema, VortexOutputWriterFactory} /** V1 fallback used for catalog tables and when Vortex is listed in @@ -64,7 +68,7 @@ final class VortexFileFormat sparkSession: SparkSession, options: Map[String, String], path: Path - ): Boolean = false + ): Boolean = VortexScan.isSplitable(VortexOptions.of(options.asJava), path) override def supportBatch( sparkSession: SparkSession, diff --git a/java/vortex-spark/common/src/main/scala/dev/vortex/spark/read/VortexScan.scala b/java/vortex-spark/common/src/main/scala/dev/vortex/spark/read/VortexScan.scala index aa62b836686..71e35da5df2 100644 --- a/java/vortex-spark/common/src/main/scala/dev/vortex/spark/read/VortexScan.scala +++ b/java/vortex-spark/common/src/main/scala/dev/vortex/spark/read/VortexScan.scala @@ -29,6 +29,16 @@ object VortexScan { /** Option turning footer-backed row-count statistics off. */ val RowCountStatisticsOption = "vortex.stats.rowCount" + + /** Option turning splitting of large files into row ranges off. */ + val SplitFilesOption = "vortex.splitFiles" + + /** Whether Spark may split `path` into byte ranges, which the reader maps to + * row ranges. + */ + def isSplitable(options: VortexOptions, path: Path): Boolean = + options.getBoolean(SplitFilesOption, true) && + VortexFile.hasVortexExtension(path.getName) } /** A Vortex scan planned by Spark's file source framework. */ @@ -58,7 +68,9 @@ final class VortexScan( private val caseSensitive = sparkSession.sessionState.conf.caseSensitiveAnalysis - override def isSplitable(path: Path): Boolean = false + // A pushed COUNT(*) reads only the footer, so splitting would just open it once per split. + override def isSplitable(path: Path): Boolean = + pushedAggregation.isEmpty && VortexScan.isSplitable(options, path) override def readSchema(): StructType = if (pushedAggregation.nonEmpty) readDataSchema else super.readSchema() @@ -118,7 +130,10 @@ final class VortexScan( private def vortexFiles = partitions .flatMap(_.files) - .filter(file => VortexFile.hasVortexExtension(file.toPath.getName)) + // A split file appears once per split, and exactly one of them starts at its first byte. + .filter(file => + file.start == 0 && VortexFile.hasVortexExtension(file.toPath.getName) + ) override def equals(other: Any): Boolean = other match { case scan: VortexScan => diff --git a/java/vortex-spark/common/src/test/java/dev/vortex/spark/VortexFileSplitTest.java b/java/vortex-spark/common/src/test/java/dev/vortex/spark/VortexFileSplitTest.java new file mode 100644 index 00000000000..6f740c2140d --- /dev/null +++ b/java/vortex-spark/common/src/test/java/dev/vortex/spark/VortexFileSplitTest.java @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +package dev.vortex.spark; + +import static org.apache.spark.sql.functions.spark_partition_id; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SaveMode; +import org.apache.spark.sql.SparkSession; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Files larger than Spark's split size are read by several tasks, through the V2 scan and the V1 file format alike, and + * together those tasks return every row exactly once. + * + *

Each test fixes {@code openCostInBytes} at 0 and {@code minPartitionNum} at 1, so the split size is + * {@code maxPartitionBytes} capped at the dataset size and the expected partition count follows from the file sizes. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public final class VortexFileSplitTest { + // Large enough that every column holds several chunks. + private static final long ROWS = 300_000; + private static final int[] SPLITS_PER_FILE = {2, 3, 7, 50}; + + private SparkSession spark; + + @TempDir + Path tempDir; + + @BeforeAll + public void setUp() { + spark = SparkSession.builder() + .appName("VortexFileSplitTest") + .master("local[2]") + .config("spark.driver.host", "127.0.0.1") + .config("spark.sql.shuffle.partitions", "2") + .config("spark.sql.adaptive.enabled", "false") + .config("spark.ui.enabled", "false") + .getOrCreate(); + } + + @AfterEach + public void resetConf() { + for (String key : List.of( + "spark.sql.files.maxPartitionBytes", + "spark.sql.files.openCostInBytes", + "spark.sql.files.minPartitionNum", + "spark.sql.files.maxPartitionNum", + "spark.sql.sources.useV1SourceList")) { + spark.conf().unset(key); + } + } + + @AfterAll + public void tearDown() { + if (spark != null) { + spark.stop(); + } + } + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"v2", "v1"}) + @DisplayName("Every split size returns each row of a many-chunk file once, from as many tasks as Spark plans") + void everySplitSizeReadsEachRowOnce(String mode) { + Path output = write(rows(ROWS).coalesce(1), "many_chunks"); + long fileSize = fileSizes(output).get(0); + // Smaller than a chunk, unaligned to anything, and larger than the file. + List splitSizes = new ArrayList<>(List.of(16_411L, 65_537L, 262_147L, 1_000_003L, fileSize + 1)); + for (int splits : SPLITS_PER_FILE) { + splitSizes.add(fileSize / splits + splits % 2); + } + + for (long splitSize : splitSizes) { + Dataset read = read(output, mode, splitSize); + String context = "mode=" + mode + " splitSize=" + splitSize + " fileSize=" + fileSize; + assertEquals( + expectedPartitions(List.of(fileSize), splitSize), read.rdd().getNumPartitions(), context); + assertEquals( + RowFactory.create(ROWS, ROWS, 0L, ROWS - 1), + read.selectExpr("count(id)", "count(distinct id)", "min(id)", "max(id)") + .first(), + context); + } + + Dataset split = read(output, mode, 65_537); + assertSameRows(rows(ROWS), split); + // Several tasks return rows, rather than one reading the whole file. + assertTrue(split.select(spark_partition_id()).distinct().count() > 1); + + // One task reading several splits of the same file. + spark.conf().set("spark.sql.files.maxPartitionNum", 1); + Dataset packed = read(output, mode, fileSize / 7 + 1); + assertEquals(1, packed.rdd().getNumPartitions()); + assertEquals( + RowFactory.create(ROWS, ROWS, 0L, ROWS - 1), + packed.selectExpr("count(id)", "count(distinct id)", "min(id)", "max(id)") + .first()); + } + + @Test + @DisplayName("A pushed COUNT(*) reads each file as one split") + void countStarCountsEachFileOnce() { + Path output = write(rows(ROWS).coalesce(1), "count"); + long splitSize = fileSizes(output).get(0) / 7; + + Dataset pushed = read(output, "v2", splitSize).selectExpr("count(*)"); + assertTrue(pushed.queryExecution().executedPlan().toString().contains("PushedAggregation: [COUNT(*)]")); + assertEquals( + 1, + pushed.queryExecution() + .executedPlan() + .collectLeaves() + .head() + .executeColumnar() + .getNumPartitions()); + assertEquals(ROWS, pushed.first().getLong(0)); + } + + @Test + @DisplayName("Scan statistics read each split file's footer once") + void statisticsCountEachSplitFileOnce() { + Path output = write(rows(ROWS).coalesce(1), "stats"); + Dataset read = read(output, "v2", fileSizes(output).get(0) / 5); + assertTrue(read.rdd().getNumPartitions() >= 5); + + var statistics = + read.queryExecution().optimizedPlan().collectLeaves().head().stats(); + assertTrue(statistics.rowCount().isDefined()); + assertEquals(ROWS, statistics.rowCount().get().longValue()); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"v2", "v1"}) + @DisplayName("vortex.splitFiles=false turns splitting off") + void splitFilesOptionTurnsSplittingOff(String mode) { + Path output = write(rows(ROWS).coalesce(1), "option"); + List sizes = fileSizes(output); + long splitSize = sizes.get(0) / 4; + assertEquals( + expectedPartitions(sizes, splitSize), + read(output, mode, splitSize).rdd().getNumPartitions()); + Dataset read = spark.read() + .format("vortex") + .option("vortex.splitFiles", "false") + .load(output.toString()); + assertEquals(1, read.rdd().getNumPartitions()); + } + + private Dataset rows(long count) { + return spark.range(count) + .selectExpr("id", "if(id % 7 = 0, null, concat('n', id)) as name", "cast(id * 3 as double) as amount"); + } + + private Path write(Dataset data, String name) { + Path output = tempDir.resolve(name); + data.write().format("vortex").mode(SaveMode.Overwrite).save(output.toString()); + return output; + } + + /** Reads through the V2 scan or the V1 file format, with Spark splitting files at {@code splitSize} bytes. */ + private Dataset read(Path output, String mode, long splitSize) { + boolean v1 = !mode.equals("v2"); + spark.conf().set("spark.sql.sources.useV1SourceList", v1 ? "vortex" : ""); + spark.conf().set("spark.sql.files.maxPartitionBytes", splitSize); + spark.conf().set("spark.sql.files.openCostInBytes", 0); + spark.conf().set("spark.sql.files.minPartitionNum", 1); + Dataset read = spark.read().format("vortex").load(output.toString()); + String plan = read.queryExecution().executedPlan().toString(); + assertTrue(plan.contains(v1 ? "FileScan vortex" : "BatchScan"), plan); + return read; + } + + private static List fileSizes(Path output) { + try (Stream files = Files.walk(output)) { + return files.filter(path -> path.toString().endsWith(".vortex")) + .map(path -> path.toFile().length()) + .sorted(Comparator.reverseOrder()) + .toList(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** Spark's split and packing rule ({@code FilePartition.getFilePartitions}) with no open cost. */ + private static int expectedPartitions(List fileSizes, long maxPartitionBytes) { + long splitSize = Math.min( + maxPartitionBytes, fileSizes.stream().mapToLong(Long::longValue).sum()); + List splits = new ArrayList<>(); + for (long fileSize : fileSizes) { + for (long start = 0; start < fileSize; start += splitSize) { + splits.add(Math.min(splitSize, fileSize - start)); + } + } + splits.sort(Comparator.reverseOrder()); + int partitions = 0; + long current = 0; + for (long split : splits) { + if (current > 0 && current + split > splitSize) { + partitions++; + current = 0; + } + current += split; + } + return current > 0 ? partitions + 1 : partitions; + } + + private static void assertSameRows(Dataset expected, Dataset actual) { + Dataset projected = actual.select(expected.columns()[0], tail(expected.columns())); + assertEquals(expected.count(), projected.count()); + assertEquals(0, expected.exceptAll(projected).count()); + assertEquals(0, projected.exceptAll(expected).count()); + } + + private static String[] tail(String[] columns) { + String[] tail = new String[columns.length - 1]; + System.arraycopy(columns, 1, tail, 0, tail.length); + return tail; + } +} diff --git a/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/SplitRowsTest.java b/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/SplitRowsTest.java new file mode 100644 index 00000000000..608eda5236f --- /dev/null +++ b/java/vortex-spark/common/src/test/java/dev/vortex/spark/read/SplitRowsTest.java @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +package dev.vortex.spark.read; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class SplitRowsTest { + + @Test + @DisplayName("Spark's byte splits of a file map to row ranges that tile its rows once, on row-block boundaries") + void byteSplitsTileTheRows() { + long[] rowCounts = {0, 1, 2, 8191, 8192, 8193, 50_000, 131_072, 1_000_003}; + long[] fileSizes = {1, 7, 600, 4096, 1_048_576, 3_600_017, 104_857_600}; + long[] splitSizes = {1, 3, 1000, 65_537, 1_048_576, 4_194_304, 134_217_728}; + for (long rows : rowCounts) { + for (long fileSize : fileSizes) { + for (long splitSize : splitSizes) { + if (fileSize / splitSize > 100_000) { + continue; + } + assertTiles(rows, fileSize, splitSize); + } + } + } + } + + @Test + @DisplayName("Offsets map at the same fraction of the file, rounded down to a row block") + void offsetsMapAtTheSameFraction() { + // A third of 100k rows is 33333, rounded down to four 8192-row blocks. + assertEquals(new SplitRows(0, 32_768), SplitRows.of(0, 1_000_000, 3_000_000, 100_000)); + assertEquals(new SplitRows(32_768, 65_536), SplitRows.of(1_000_000, 1_000_000, 3_000_000, 100_000)); + assertEquals(new SplitRows(65_536, 100_000), SplitRows.of(2_000_000, 1_000_000, 3_000_000, 100_000)); + // A split smaller than a row block's worth of bytes may read nothing; a neighbour reads its rows. + assertEquals(new SplitRows(0, 0), SplitRows.of(0, 1000, 3_000_000, 100_000)); + } + + @Test + @DisplayName("Offsets times rows beyond a long still map exactly") + void largeFilesDoNotOverflow() { + long fileSize = 1L << 42; + long rows = 1L << 40; + SplitRows half = SplitRows.of(fileSize / 2, fileSize / 2, fileSize, rows); + assertEquals(new SplitRows(rows / 2, rows), half); + } + + private static void assertTiles(long rows, long fileSize, long splitSize) { + List ranges = new ArrayList<>(); + // Spark's PartitionedFileUtil.splitFiles: contiguous ranges of splitSize, the last one shorter. + for (long start = 0; start < fileSize; start += splitSize) { + ranges.add(SplitRows.of(start, Math.min(splitSize, fileSize - start), fileSize, rows)); + } + String context = String.format("rows=%d fileSize=%d splitSize=%d", rows, fileSize, splitSize); + long next = 0; + for (SplitRows range : ranges) { + assertEquals(next, range.begin(), context); + assertTrue(range.end() >= range.begin(), context); + assertTrue(range.end() == rows || range.end() % SplitRows.ROW_BLOCK == 0, context); + next = range.end(); + } + assertEquals(rows, next, context); + } +} From e42a7b5de8cd7339ff933dd208183bc4b85546e1 Mon Sep 17 00:00:00 2001 From: Mikhail Kot Date: Wed, 4 Nov 2026 16:53:40 +0000 Subject: [PATCH 25/41] Property-based tests for Mask (#11167) Mask tests test invariants of exact inputs and outputs. We do want to test object's observed properties of functions available in the public API. This is my attempt at replacing some of Mask's tests with ones based on Hegel. The new tests generate random inputs and check properties of operations on Masks still hold. This change also fixes operator== which worked incorrectly for empty Masks, returning false. --------- Signed-off-by: Mikhail Kot --- Cargo.lock | 1 + vortex-mask/Cargo.toml | 1 + vortex-mask/src/bitops.rs | 318 --------- vortex-mask/src/eq.rs | 133 +--- vortex-mask/src/intersect_by_rank/mod.rs | 133 ---- vortex-mask/src/tests.rs | 831 ++++++++--------------- 6 files changed, 299 insertions(+), 1118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f0171d8a02..d2e081a74a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12259,6 +12259,7 @@ name = "vortex-mask" version = "0.1.0" dependencies = [ "codspeed-divan-compat", + "hegeltest", "itertools 0.14.0", "mimalloc", "rand 0.10.3", diff --git a/vortex-mask/Cargo.toml b/vortex-mask/Cargo.toml index c0f1f5fb218..58ee17dc7fe 100644 --- a/vortex-mask/Cargo.toml +++ b/vortex-mask/Cargo.toml @@ -26,6 +26,7 @@ vortex-error = { workspace = true } [dev-dependencies] divan = { workspace = true } +hegeltest = { workspace = true } mimalloc = { workspace = true } rand = { workspace = true } rstest = { workspace = true } diff --git a/vortex-mask/src/bitops.rs b/vortex-mask/src/bitops.rs index e795c57204f..3fac559f3ac 100644 --- a/vortex-mask/src/bitops.rs +++ b/vortex-mask/src/bitops.rs @@ -122,321 +122,3 @@ impl Not for &Mask { } } } - -#[cfg(test)] -#[expect(clippy::many_single_char_names)] -mod tests { - use vortex_buffer::BitBuffer; - - use super::*; - - #[test] - fn test_bitand_all_combinations() { - let len = 5; - - // Test AllTrue & AllTrue - let all_true = Mask::new_true(len); - let result = &all_true & &all_true; - assert!(result.all_true()); - assert_eq!(result.true_count(), len); - - // Test AllTrue & AllFalse - let all_false = Mask::new_false(len); - let result = &all_true & &all_false; - assert!(result.all_false()); - assert_eq!(result.true_count(), 0); - - // Test AllFalse & AllTrue - let result = &all_false & &all_true; - assert!(result.all_false()); - assert_eq!(result.true_count(), 0); - - // Test AllFalse & AllFalse - let result = &all_false & &all_false; - assert!(result.all_false()); - assert_eq!(result.true_count(), 0); - } - - #[test] - fn test_bitand_with_values() { - let mask1 = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let mask2 = Mask::from_buffer(BitBuffer::from_iter([true, true, false, false, true])); - - let result = &mask1 & &mask2; - assert_eq!(result.len(), 5); - assert_eq!(result.true_count(), 2); - assert!(result.value(0)); // true & true = true - assert!(!result.value(1)); // false & true = false - assert!(!result.value(2)); // true & false = false - assert!(!result.value(3)); // false & false = false - assert!(result.value(4)); // true & true = true - } - - #[test] - fn test_bitand_all_true_with_values() { - let all_true = Mask::new_true(5); - let values = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - - // AllTrue & Values should return Values - let result = &all_true & &values; - assert_eq!(result.true_count(), 3); - assert_eq!(result.len(), 5); - assert!(result.value(0)); - assert!(!result.value(1)); - assert!(result.value(2)); - assert!(!result.value(3)); - assert!(result.value(4)); - } - - #[test] - fn test_bitand_all_false_with_values() { - let all_false = Mask::new_false(5); - let values = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - - // AllFalse & Values should return AllFalse - let result = &all_false & &values; - assert!(result.all_false()); - assert_eq!(result.true_count(), 0); - } - - #[test] - fn test_bitand_values_with_all_true() { - let values = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let all_true = Mask::new_true(5); - - // Values & AllTrue should return Values - let result = &values & &all_true; - assert_eq!(result.true_count(), 3); - assert_eq!(result.len(), 5); - assert!(result.value(0)); - assert!(!result.value(1)); - assert!(result.value(2)); - assert!(!result.value(3)); - assert!(result.value(4)); - } - - #[test] - fn test_bitand_values_with_all_false() { - let values = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let all_false = Mask::new_false(5); - - // Values & AllFalse should return AllFalse - let result = &values & &all_false; - assert!(result.all_false()); - assert_eq!(result.true_count(), 0); - } - - #[test] - fn test_bitand_empty_masks() { - let empty1 = Mask::new_true(0); - let empty2 = Mask::new_false(0); - - let result = &empty1 & &empty2; - assert_eq!(result.len(), 0); - assert!(result.is_empty()); - } - - #[test] - #[should_panic(expected = "Masks must have the same length")] - fn test_bitand_different_lengths() { - let mask1 = Mask::new_true(5); - let mask2 = Mask::new_true(3); - let _unused = &mask1 & &mask2; - } - - #[test] - fn test_not_all_true() { - let all_true = Mask::new_true(5); - let result = !&all_true; - assert!(result.all_false()); - assert_eq!(result.true_count(), 0); - assert_eq!(result.len(), 5); - } - - #[test] - fn test_not_all_false() { - let all_false = Mask::new_false(5); - let result = !&all_false; - assert!(result.all_true()); - assert_eq!(result.true_count(), 5); - assert_eq!(result.len(), 5); - } - - #[test] - fn test_not_values() { - let values = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let result = !&values; - - assert_eq!(result.len(), 5); - assert_eq!(result.true_count(), 2); - assert!(!result.value(0)); // !true = false - assert!(result.value(1)); // !false = true - assert!(!result.value(2)); // !true = false - assert!(result.value(3)); // !false = true - assert!(!result.value(4)); // !true = false - } - - #[test] - fn test_not_empty() { - let empty_true = Mask::new_true(0); - let result = !&empty_true; - assert_eq!(result.len(), 0); - assert!(result.is_empty()); - - let empty_false = Mask::new_false(0); - let result = !&empty_false; - assert_eq!(result.len(), 0); - assert!(result.is_empty()); - } - - #[test] - fn test_double_not() { - let original = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let double_not = !&(!&original); - - // Double negation should return the original - assert_eq!(double_not.true_count(), original.true_count()); - for i in 0..5 { - assert_eq!(double_not.value(i), original.value(i)); - } - } - - #[test] - fn test_demorgan_law() { - // Test De Morgan's law: !(A & B) = !A | !B - let a = Mask::from_buffer(BitBuffer::from_iter([true, true, false, false])); - let b = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false])); - - let and_result = &a & &b; - let not_and = !&and_result; - - let not_a = !&a; - let not_b = !&b; - let or_result = ¬_a | ¬_b; - - assert_eq!(not_and.len(), 4); - assert!(!not_and.value(0)); // !(true & true) = false - assert!(not_and.value(1)); // !(true & false) = true - assert!(not_and.value(2)); // !(false & true) = true - assert!(not_and.value(3)); // !(false & false) = true - - assert_eq!(or_result.len(), 4); - assert_eq!(or_result, not_and) - } - - #[test] - fn test_bitand_associativity() { - // Test (A & B) & C = A & (B & C) - let a = Mask::from_buffer(BitBuffer::from_iter([true, true, false, true])); - let b = Mask::from_buffer(BitBuffer::from_iter([true, false, true, true])); - let c = Mask::from_buffer(BitBuffer::from_iter([false, true, true, true])); - - let left_assoc = &(&a & &b) & &c; - let right_assoc = &a & &(&b & &c); - - assert_eq!(left_assoc.true_count(), right_assoc.true_count()); - for i in 0..4 { - assert_eq!(left_assoc.value(i), right_assoc.value(i)); - } - } - - #[test] - fn test_bitand_commutativity() { - // Test A & B = B & A - let a = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false])); - let b = Mask::from_buffer(BitBuffer::from_iter([false, true, false, true])); - - let a_and_b = &a & &b; - let b_and_a = &b & &a; - - assert_eq!(a_and_b.true_count(), b_and_a.true_count()); - for i in 0..4 { - assert_eq!(a_and_b.value(i), b_and_a.value(i)); - } - } - - #[test] - fn test_bitand_idempotence() { - // Test A & A = A - let mask = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let result = &mask & &mask; - - assert_eq!(result.true_count(), mask.true_count()); - for i in 0..5 { - assert_eq!(result.value(i), mask.value(i)); - } - } - - #[test] - fn test_complex_expression() { - // Test a more complex expression: (!(!A) | B) & !C - let a = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false])); - let b = Mask::from_buffer(BitBuffer::from_iter([true, true, false, false])); - let c = Mask::from_buffer(BitBuffer::from_iter([false, true, false, true])); - - let not_not_a = !(&(!&a)); - let not_not_a_or_b = ¬_not_a | &b; - let not_c = !&c; - let result = ¬_not_a_or_b & ¬_c; - - // Verify the result manually - assert!(result.value(0)); // (!(!true) | true) & !false = (true | true) & true = true - assert!(!result.value(1)); // (!(!false) | true) & !true = (false | true) & false = false - assert!(result.value(2)); // (!(!true) | false) & !false = (true | false) & true = true - assert!(!result.value(3)); // (!(!false) | false) & !true = (false | false) & false = false - } - - #[test] - fn test_bitand_not() { - let a = Mask::from_buffer(BitBuffer::from_iter([true, true, false, false])); - let b = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false])); - let result = a.clone().bitand_not(&b); - assert!(!result.value(0)); // true & !true = false - assert!(result.value(1)); // true & !false = true - assert!(!result.value(2)); // false & !true = false - assert!(!result.value(3)); // false & !false = false - - // bitand_not(All) = None - assert!(a.clone().bitand_not(&Mask::new_true(4)).all_false()); - - // bitand_not(None) = self - let none = Mask::new_false(4); - assert_eq!(a.clone().bitand_not(&none), a); - - // None.bitand_not(_) = None - assert!(none.bitand_not(&a).all_false()); - - // All.bitand_not(x) = !x - let not_b = !&b; - let all_bitand_not_b = Mask::new_true(4).bitand_not(&b); - for i in 0..4 { - assert_eq!(all_bitand_not_b.value(i), not_b.value(i)); - } - } - - #[test] - fn test_bitor() { - // Test basic OR operations - let mask1 = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let mask2 = Mask::from_buffer(BitBuffer::from_iter([true, true, false, false, true])); - - let result = &mask1 | &mask2; - assert_eq!(result.len(), 5); - assert_eq!(result.true_count(), 4); - assert!(result.value(0)); // true | true = true - assert!(result.value(1)); // false | true = true - assert!(result.value(2)); // true | false = true - assert!(!result.value(3)); // false | false = false - assert!(result.value(4)); // true | true = true - - // Test with AllTrue - let all_true = Mask::new_true(5); - let result = &mask1 | &all_true; - assert!(result.all_true()); - - // Test with AllFalse - let all_false = Mask::new_false(5); - let result = &mask1 | &all_false; - assert_eq!(result, mask1); - } -} diff --git a/vortex-mask/src/eq.rs b/vortex-mask/src/eq.rs index c36ead6d8fd..9ba69eb6c27 100644 --- a/vortex-mask/src/eq.rs +++ b/vortex-mask/src/eq.rs @@ -11,6 +11,9 @@ impl PartialEq for Mask { if self.len() != other.len() { return false; } + if self.is_empty() { + return true; + } if mem::discriminant(self) == mem::discriminant(other) && !matches!(self, Mask::Values(_)) { return true; } @@ -24,133 +27,3 @@ impl PartialEq for Mask { } impl Eq for Mask {} - -#[cfg(test)] -mod test { - use vortex_buffer::BitBuffer; - - use crate::Mask; - - #[test] - fn filter_mask_eq() { - assert_eq!(Mask::new_true(5), Mask::from_buffer(BitBuffer::new_set(5))); - assert_eq!( - Mask::new_false(5), - Mask::from_buffer(BitBuffer::new_unset(5)) - ); - assert_eq!( - Mask::from_indices(5, vec![0, 2, 3]), - Mask::from_slices(5, vec![(0, 1), (2, 4)]) - ); - assert_eq!( - Mask::from_indices(5, vec![0, 2, 3]), - Mask::from_buffer(BitBuffer::from_iter([true, false, true, true, false])) - ); - } - - #[test] - fn test_mask_eq_different_lengths() { - let mask1 = Mask::new_true(5); - let mask2 = Mask::new_true(3); - assert_ne!(mask1, mask2); - } - - #[test] - fn test_mask_eq_different_true_counts() { - let mask1 = Mask::from_buffer(BitBuffer::from_iter([true, true, false])); - let mask2 = Mask::from_buffer(BitBuffer::from_iter([true, false, false])); - assert_ne!(mask1, mask2); - } - - #[test] - fn test_mask_eq_same_count_different_positions() { - let mask1 = Mask::from_buffer(BitBuffer::from_iter([true, false, false])); - let mask2 = Mask::from_buffer(BitBuffer::from_iter([false, true, false])); - assert_ne!(mask1, mask2); - } - - #[test] - fn test_mask_eq_all_variants() { - // Test AllTrue == AllTrue - let all_true1 = Mask::new_true(5); - let all_true2 = Mask::new_true(5); - assert_eq!(all_true1, all_true2); - - // Test AllFalse == AllFalse - let all_false1 = Mask::new_false(5); - let all_false2 = Mask::new_false(5); - assert_eq!(all_false1, all_false2); - - // Test AllTrue != AllFalse - assert_ne!(all_true1, all_false1); - - // Test Values == Values - let values1 = Mask::from_buffer(BitBuffer::from_iter([true, false, true])); - let values2 = Mask::from_buffer(BitBuffer::from_iter([true, false, true])); - assert_eq!(values1, values2); - - // Test AllTrue != Values (even if all values are true) - let all_true_values = Mask::from_buffer(BitBuffer::new_set(5)); - assert_eq!(all_true1, all_true_values); // They should be equal - - // Test AllFalse != Values (even if all values are false) - let all_false_values = Mask::from_buffer(BitBuffer::new_unset(5)); - assert_eq!(all_false1, all_false_values); // They should be equal - } - - #[test] - fn test_mask_eq_symmetric() { - // Test that if a == b then b == a - let mask1 = Mask::from_indices(5, vec![0, 2, 4]); - let mask2 = Mask::from_slices(5, vec![(0, 1), (2, 3), (4, 5)]); - assert_eq!(mask1, mask2); - assert_eq!(mask2, mask1); - } - - #[test] - fn test_mask_eq_transitive() { - // Test that if a == b and b == c then a == c - let mask1 = Mask::from_indices(5, vec![1, 3]); - let mask2 = Mask::from_slices(5, vec![(1, 2), (3, 4)]); - let mask3 = Mask::from_buffer(BitBuffer::from_iter([false, true, false, true, false])); - - assert_eq!(mask1, mask2); - assert_eq!(mask2, mask3); - assert_eq!(mask1, mask3); - } - - #[test] - fn test_mask_eq_empty() { - // All empty masks become AllFalse regardless of input type - let empty1 = Mask::new_true(0); - let empty2 = Mask::new_false(0); - let empty3 = Mask::from_buffer(BitBuffer::new_set(0)); - let empty4 = Mask::from_buffer(BitBuffer::new_unset(0)); - - // All should be AllFalse(0) when created from buffer - assert!(matches!(empty3, Mask::AllFalse(0))); - assert!(matches!(empty4, Mask::AllFalse(0))); - - // new_true(0) is AllTrue(0), new_false(0) is AllFalse(0) - assert!(matches!(empty1, Mask::AllTrue(0))); - assert!(matches!(empty2, Mask::AllFalse(0))); - } - - #[test] - fn test_mask_eq_different_representations() { - // Test that masks with the same logical values but different internal representations are equal - let indices = vec![0, 1, 2, 5, 6, 9]; - let slices = vec![(0, 3), (5, 7), (9, 10)]; - let buffer = BitBuffer::from_iter([ - true, true, true, false, false, true, true, false, false, true, - ]); - - let mask1 = Mask::from_indices(10, indices); - let mask2 = Mask::from_slices(10, slices); - let mask3 = Mask::from_buffer(buffer); - - assert_eq!(mask1, mask2); - assert_eq!(mask2, mask3); - assert_eq!(mask1, mask3); - } -} diff --git a/vortex-mask/src/intersect_by_rank/mod.rs b/vortex-mask/src/intersect_by_rank/mod.rs index eff0f5ef2fe..1fe17f2ac8d 100644 --- a/vortex-mask/src/intersect_by_rank/mod.rs +++ b/vortex-mask/src/intersect_by_rank/mod.rs @@ -659,101 +659,6 @@ mod tests { use super::sve::Sve2Bdep; use crate::Mask; - #[test] - fn mask_bitand_all_as_bit_and() { - let this = Mask::from_buffer(BitBuffer::from_iter(vec![true, true, true, true, true])); - let mask = Mask::from_buffer(BitBuffer::from_iter(vec![false, true, false, true, true])); - assert_eq!( - this.intersect_by_rank(&mask), - Mask::from_indices(5, vec![1, 3, 4]) - ); - } - - #[test] - fn mask_bitand_all_true() { - let this = Mask::from_buffer(BitBuffer::from_iter(vec![false, false, true, true, true])); - let mask = Mask::from_buffer(BitBuffer::from_iter(vec![true, true, true])); - assert_eq!( - this.intersect_by_rank(&mask), - Mask::from_indices(5, vec![2, 3, 4]) - ); - } - - #[test] - fn mask_bitand_true() { - let this = Mask::from_buffer(BitBuffer::from_iter(vec![true, false, false, true, true])); - let mask = Mask::from_buffer(BitBuffer::from_iter(vec![true, false, true])); - assert_eq!( - this.intersect_by_rank(&mask), - Mask::from_indices(5, vec![0, 4]) - ); - } - - #[test] - fn mask_bitand_false() { - let this = Mask::from_buffer(BitBuffer::from_iter(vec![true, false, false, true, true])); - let mask = Mask::from_buffer(BitBuffer::from_iter(vec![false, false, false])); - assert_eq!(this.intersect_by_rank(&mask), Mask::from_indices(5, vec![])); - } - - #[test] - fn mask_intersect_by_rank_all_false() { - let this = Mask::AllFalse(10); - let mask = Mask::AllFalse(0); - assert_eq!(this.intersect_by_rank(&mask), Mask::AllFalse(10)); - } - - #[rstest] - #[case::all_true_with_all_true( - Mask::new_true(5), - Mask::new_true(5), - vec![0, 1, 2, 3, 4] - )] - #[case::all_true_with_all_false( - Mask::new_true(5), - Mask::new_false(5), - vec![] - )] - #[case::all_false_with_any( - Mask::new_false(10), - Mask::new_true(0), - vec![] - )] - #[case::indices_with_all_true( - Mask::from_indices(10, vec![2, 5, 7, 9]), - Mask::new_true(4), - vec![2, 5, 7, 9] - )] - #[case::indices_with_all_false( - Mask::from_indices(10, vec![2, 5, 7, 9]), - Mask::new_false(4), - vec![] - )] - fn test_intersect_by_rank_special_cases( - #[case] base_mask: Mask, - #[case] rank_mask: Mask, - #[case] expected_indices: Vec, - ) { - let result = base_mask.intersect_by_rank(&rank_mask); - assert_eq!(result.len(), base_mask.len()); - - match result.indices() { - crate::AllOr::All => assert_eq!(expected_indices.len(), result.len()), - crate::AllOr::None => assert!(expected_indices.is_empty()), - crate::AllOr::Some(indices) => assert_eq!(indices, &expected_indices[..]), - } - } - - #[test] - fn test_intersect_by_rank_example() { - // Example from the documentation - let m1 = Mask::from_iter([true, false, false, true, true, true, false, true]); - let m2 = Mask::from_iter([false, false, true, false, true]); - let result = m1.intersect_by_rank(&m2); - let expected = Mask::from_iter([false, false, false, false, true, false, false, true]); - assert_eq!(result, expected); - } - #[test] #[should_panic] fn test_intersect_by_rank_wrong_length() { @@ -762,44 +667,6 @@ mod tests { m1.intersect_by_rank(&m2); } - #[rstest] - #[case::single_element( - vec![3], - vec![true], - vec![3] - )] - #[case::single_element_masked( - vec![3], - vec![false], - vec![] - )] - #[case::alternating( - vec![0, 2, 4, 6, 8], - vec![true, false, true, false, true], - vec![0, 4, 8] - )] - #[case::consecutive( - vec![5, 6, 7, 8, 9], - vec![false, true, true, true, false], - vec![6, 7, 8] - )] - fn test_intersect_by_rank_patterns( - #[case] base_indices: Vec, - #[case] rank_pattern: Vec, - #[case] expected_indices: Vec, - ) { - let base = Mask::from_indices(10, base_indices); - let rank = Mask::from_iter(rank_pattern); - let result = base.intersect_by_rank(&rank); - assert_eq!(result.len(), base.len()); - - match result.indices() { - crate::AllOr::Some(indices) => assert_eq!(indices, &expected_indices[..]), - crate::AllOr::None => assert!(expected_indices.is_empty()), - _ => panic!("Unexpected result"), - } - } - #[rstest] // Larger sizes to push the bench-shaped buffer paths through the unit tests too. #[case::dense_len_1024(1024, 31, 0.5, 0.5)] diff --git a/vortex-mask/src/tests.rs b/vortex-mask/src/tests.rs index b888269f253..0b4bdeb1316 100644 --- a/vortex-mask/src/tests.rs +++ b/vortex-mask/src/tests.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use hegel::TestCase; +use hegel::generators as gs; use rstest::rstest; use vortex_buffer::BitBuffer; @@ -8,252 +10,327 @@ use crate::AllOr; use crate::Mask; use crate::MaskIter; -// Basic mask creation and properties tests -#[test] -fn mask_all_true() { - let mask = Mask::new_true(5); - assert_eq!(mask.len(), 5); - assert_eq!(mask.true_count(), 5); - assert_eq!(mask.density(), 1.0); - assert_eq!(mask.indices(), AllOr::All); - assert_eq!(mask.slices(), AllOr::All); - assert_eq!(mask.bit_buffer(), AllOr::All,); -} +const MAX_LEN: usize = 200; -#[test] -fn mask_all_false() { - let mask = Mask::new_false(5); - assert_eq!(mask.len(), 5); - assert_eq!(mask.true_count(), 0); - assert_eq!(mask.density(), 0.0); - assert_eq!(mask.indices(), AllOr::None); - assert_eq!(mask.slices(), AllOr::None); - assert_eq!(mask.bit_buffer(), AllOr::None,); +fn draw_len(tc: &TestCase) -> usize { + tc.draw(gs::integers::().max_value(MAX_LEN)) } -#[test] -fn mask_from() { - let masks = [ - Mask::from_indices(5, vec![0, 2, 3]), - Mask::from_slices(5, vec![(0, 1), (2, 4)]), - Mask::from_buffer(BitBuffer::from_iter([true, false, true, true, false])), - ]; - - for mask in &masks { - assert_eq!(mask.len(), 5); - assert_eq!(mask.true_count(), 3); - assert_eq!(mask.density(), 0.6); - assert_eq!(mask.indices(), AllOr::Some(&[0, 2, 3][..])); - assert_eq!(mask.slices(), AllOr::Some(&[(0, 1), (2, 4)][..])); - assert_eq!( - mask.bit_buffer(), - AllOr::Some(&BitBuffer::from_iter([true, false, true, true, false])) - ); +fn draw_bits(tc: &TestCase, len: usize) -> Vec { + match tc.draw(gs::integers::().max_value(2)) { + 0 => vec![true; len], + 1 => vec![false; len], + _ => tc.draw(gs::vecs(gs::booleans()).min_size(len).max_size(len)), } } -#[test] -fn length_zero_masks() { - let all_false = Mask::new_false(0); - let all_true = Mask::new_true(0); - let buffer_set = Mask::from_buffer(BitBuffer::new_set(0)); - let buffer_unset = Mask::from_buffer(BitBuffer::new_unset(0)); - - assert!(all_false.all_false()); - assert!(all_false.all_true()); - assert!(all_true.all_false()); - assert!(all_true.all_true()); - assert!(buffer_set.all_false()); - assert!(buffer_set.all_true()); - assert!(buffer_unset.all_false()); - assert!(buffer_unset.all_true()); -} - -// Mask value access tests -#[test] -fn test_mask_value() { - let all_true = Mask::new_true(5); - assert!(all_true.value(0)); - assert!(all_true.value(4)); - - let all_false = Mask::new_false(5); - assert!(!all_false.value(0)); - assert!(!all_false.value(4)); - - let values = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - assert!(values.value(0)); - assert!(!values.value(1)); - assert!(values.value(2)); - assert!(!values.value(3)); - assert!(values.value(4)); +fn build_mask(tc: &TestCase, bits: &[bool]) -> Mask { + let len = bits.len(); + match tc.draw(gs::integers::().max_value(4)) { + 0 if bits.iter().all(|&b| b) => Mask::new_true(len), + 0 if !bits.iter().any(|&b| b) => Mask::new_false(len), + 0 | 1 => Mask::from_iter(bits.iter().copied()), + 2 => Mask::from_indices(len, model_indices(bits)), + 3 => Mask::from_slices(len, model_slices(bits)), + _ => { + let offset = tc.draw(gs::integers::().max_value(63)); + let mut source = vec![true; offset]; + source.extend_from_slice(bits); + source.extend([false, true, false]); + Mask::from_buffer(BitBuffer::from(source).slice(offset..offset + len)) + } + } } -#[test] -fn test_mask_first() { - assert_eq!(Mask::new_true(5).first(), Some(0)); - assert_eq!(Mask::new_false(5).first(), None); - assert_eq!(Mask::new_true(0).first(), None); - - let values = Mask::from_buffer(BitBuffer::from_iter([false, false, true, false, true])); - assert_eq!(values.first(), Some(2)); - - let values_indices = Mask::from_indices(5, vec![2, 4]); - assert_eq!(values_indices.first(), Some(2)); - - let values_slices = Mask::from_slices(5, vec![(2, 3), (4, 5)]); - assert_eq!(values_slices.first(), Some(2)); +fn draw_mask_of_len(tc: &TestCase, len: usize) -> (Mask, Vec) { + let bits = draw_bits(tc, len); + (build_mask(tc, &bits), bits) } -#[test] -fn test_mask_last() { - assert_eq!(Mask::new_true(5).last(), Some(4)); - assert_eq!(Mask::new_false(5).last(), None); - assert_eq!(Mask::new_true(0).last(), None); - - let buffer = BitBuffer::from_iter([true, false, true, false, true, false, true]); - let values = Mask::from_buffer(buffer.slice(1..6)); - assert_eq!(values.last(), Some(3)); - - let values_indices = Mask::from_indices(5, vec![1, 3]); - assert_eq!(values_indices.last(), Some(3)); - - let values_slices = Mask::from_slices(5, vec![(1, 2), (3, 4)]); - assert_eq!(values_slices.last(), Some(3)); +fn draw_mask(tc: &TestCase) -> (Mask, Vec) { + let len = draw_len(tc); + draw_mask_of_len(tc, len) } -// Slice operations -#[test] -fn test_mask_slice() { - let mask = Mask::from_buffer(BitBuffer::from_iter([true, false, true, true, false])); - - let sliced = mask.slice(1..4); - assert_eq!(sliced.len(), 3); - assert_eq!(sliced.true_count(), 2); - assert!(!sliced.value(0)); // false from index 1 - assert!(sliced.value(1)); // true from index 2 - assert!(sliced.value(2)); // true from index 3 - - let all_true = Mask::new_true(10); - let sliced_true = all_true.slice(2..7); - assert!(sliced_true.all_true()); - assert_eq!(sliced_true.len(), 5); - - let all_false = Mask::new_false(10); - let sliced_false = all_false.slice(2..7); - assert!(sliced_false.all_false()); - assert_eq!(sliced_false.len(), 5); +fn model_indices(bits: &[bool]) -> Vec { + bits.iter() + .enumerate() + .filter_map(|(i, &b)| b.then_some(i)) + .collect() } -#[test] -#[should_panic] -fn test_mask_slice_out_of_bounds() { - let mask = Mask::new_true(5); - mask.slice(3..8); // offset + length > len +fn model_slices(bits: &[bool]) -> Vec<(usize, usize)> { + let mut slices: Vec<(usize, usize)> = Vec::new(); + for i in model_indices(bits) { + match slices.last_mut() { + Some((_, end)) if *end == i => *end += 1, + _ => slices.push((i, i + 1)), + } + } + slices +} + +fn assert_matches_model(mask: &Mask, bits: &[bool]) { + let indices = model_indices(bits); + let slices = model_slices(bits); + assert_counts_match(mask, bits.len(), &indices); + assert_elements_match(mask, bits, &indices); + assert_representations_match(mask, bits, &indices, &slices); + assert_values_match(mask, bits, &indices, &slices); +} + +fn assert_counts_match(mask: &Mask, len: usize, indices: &[usize]) { + assert_eq!(mask.len(), len); + assert_eq!(mask.is_empty(), len == 0); + assert_eq!(mask.true_count(), indices.len()); + assert_eq!(mask.false_count(), len - indices.len()); + assert_eq!(mask.all_true(), indices.len() == len); + assert_eq!(mask.all_false(), indices.is_empty()); + if len > 0 { + #[expect(clippy::cast_precision_loss)] + let density = indices.len() as f64 / len as f64; + assert!((mask.density() - density).abs() < 1e-10); + } } -// Limit operations -#[test] -fn limit_all_true_mask() { - let all_true = Mask::new_true(4); - let limited_mask = all_true.clone().limit(2); - assert_eq!(all_true.len(), limited_mask.len()); - assert_eq!(limited_mask.true_count(), 2); - assert_eq!( - limited_mask.bit_buffer(), - AllOr::Some(&BitBuffer::from_iter([true, true, false, false])) - ); - - let limited_mask = all_true.clone().limit(5); - assert_eq!(limited_mask, all_true); +fn assert_elements_match(mask: &Mask, bits: &[bool], indices: &[usize]) { + let len = bits.len(); + assert_eq!(mask.iter().collect::>(), bits); + assert_eq!(mask.iter().len(), len); + assert_eq!((0..len).map(|i| mask.value(i)).collect::>(), bits); + assert_eq!(mask.first(), indices.first().copied()); + assert_eq!(mask.last(), indices.last().copied()); + for (n, &idx) in indices.iter().enumerate() { + assert_eq!(mask.rank(n), idx); + } } -#[test] -fn limit_mask_values() { - let original_mask = Mask::from_iter([true, true, false, true, false, true]); - let limited_mask = original_mask.clone().limit(2); - - assert_eq!( - limited_mask.bit_buffer(), - AllOr::Some(&BitBuffer::from_iter([ - true, true, false, false, false, false - ])) - ); - assert_eq!(limited_mask.true_count(), 2); - - let limited_mask = original_mask.limit(3); - - assert_eq!( - limited_mask.bit_buffer(), - AllOr::Some(&BitBuffer::from_iter([ - true, true, false, true, false, false - ])) +fn assert_representations_match( + mask: &Mask, + bits: &[bool], + indices: &[usize], + slices: &[(usize, usize)], +) { + let len = bits.len(); + let mask_indices = match mask.indices() { + AllOr::All => (0..len).collect(), + AllOr::None => Vec::new(), + AllOr::Some(indices) => indices.to_vec(), + }; + assert_eq!(mask_indices, indices); + + let mask_slices = match mask.slices() { + AllOr::All if len > 0 => vec![(0, len)], + AllOr::All | AllOr::None => Vec::new(), + AllOr::Some(slices) => slices.to_vec(), + }; + assert_eq!(mask_slices, slices); + + let buffer_bits = match mask.bit_buffer() { + AllOr::All => vec![true; len], + AllOr::None => vec![false; len], + AllOr::Some(buffer) => buffer.iter().collect(), + }; + assert_eq!(buffer_bits, bits); + assert_eq!(mask.to_bit_buffer().iter().collect::>(), bits); +} + +fn assert_values_match(mask: &Mask, bits: &[bool], indices: &[usize], slices: &[(usize, usize)]) { + let len = bits.len(); + let Some(values) = mask.values() else { + return; + }; + assert!(!values.is_empty()); + assert_eq!(values.len(), len); + assert_eq!(values.true_count(), indices.len()); + assert_eq!(values.indices(), indices); + assert_eq!(values.slices(), slices); + assert_eq!(values.bit_buffer().iter().collect::>(), bits); + assert_eq!((0..len).map(|i| values.value(i)).collect::>(), bits); +} + +#[hegel::test] +fn mask(tc: TestCase) { + let (mask, bits) = draw_mask(&tc); + assert_matches_model(&mask, &bits); +} + +#[hegel::test] +fn mask_eq(tc: TestCase) { + let (lhs, lhs_bits) = draw_mask(&tc); + let rhs_bits = if tc.draw(gs::booleans()) { + lhs_bits.clone() + } else { + let len = if tc.draw(gs::booleans()) { + lhs_bits.len() + } else { + draw_len(&tc) + }; + draw_bits(&tc, len) + }; + let rhs = build_mask(&tc, &rhs_bits); + + assert_eq!(lhs == rhs, lhs_bits == rhs_bits); + assert_eq!(rhs == lhs, lhs_bits == rhs_bits); +} + +#[hegel::test] +fn mask_slice(tc: TestCase) { + let (mask, bits) = draw_mask(&tc); + let end = tc.draw(gs::integers::().max_value(bits.len())); + let start = tc.draw(gs::integers::().max_value(end)); + assert_matches_model(&mask.slice(start..end), &bits[start..end]); +} + +#[hegel::test] +fn mask_limit(tc: TestCase) { + let (mask, bits) = draw_mask(&tc); + let limit = tc.draw(gs::integers::().max_value(bits.len() + 2)); + let mut remaining = limit; + let expected: Vec = bits + .iter() + .map(|&b| { + let keep = b && remaining > 0; + if keep { + remaining -= 1; + } + keep + }) + .collect(); + assert_matches_model(&mask.limit(limit), &expected); +} + +#[hegel::test] +fn mask_bitops(tc: TestCase) { + let len = draw_len(&tc); + let (lhs, lhs_bits) = draw_mask_of_len(&tc, len); + let (rhs, rhs_bits) = draw_mask_of_len(&tc, len); + let zip = |f: fn(bool, bool) -> bool| -> Vec { + lhs_bits + .iter() + .zip(&rhs_bits) + .map(|(&l, &r)| f(l, r)) + .collect() + }; + + let and = zip(|l, r| l & r); + let or = zip(|l, r| l | r); + let and_not = zip(|l, r| l & !r); + let not: Vec = lhs_bits.iter().map(|&b| !b).collect(); + + assert_matches_model(&(&lhs & &rhs), &and); + assert_matches_model(&(lhs.clone() & &rhs), &and); + assert_matches_model(&(&lhs | &rhs), &or); + assert_matches_model(&(lhs.clone() | &rhs), &or); + assert_matches_model(&lhs.clone().bitand_not(&rhs), &and_not); + assert_matches_model(&!&lhs, ¬); + assert_matches_model(&!lhs, ¬); +} + +#[hegel::test] +fn mask_concat(tc: TestCase) { + let count = tc.draw(gs::integers::().max_value(4)); + let (masks, bits): (Vec, Vec>) = (0..count) + .map(|_| { + let len = tc.draw(gs::integers::().max_value(MAX_LEN / 4)); + draw_mask_of_len(&tc, len) + }) + .unzip(); + let expected = bits.concat(); + + assert_matches_model(&Mask::concat(masks.iter()).unwrap(), &expected); + assert_matches_model(&Mask::from_iter(masks), &expected); +} + +#[hegel::test] +fn mask_from_excluded(tc: TestCase) { + let len = draw_len(&tc); + let bits = draw_bits(&tc, len); + let mut excluded = Vec::new(); + for (i, _) in bits.iter().enumerate().filter(|&(_, &b)| !b) { + let copies = tc.draw(gs::integers::().min_value(1).max_value(3)); + for _ in 0..copies { + let pos = tc.draw(gs::integers::().max_value(excluded.len())); + excluded.insert(pos, i); + } + } + assert_matches_model(&Mask::from_excluded_indices(len, excluded), &bits); +} + +#[hegel::test] +fn mask_from_intersection(tc: TestCase) { + let len = draw_len(&tc); + let lhs = draw_bits(&tc, len); + let rhs = draw_bits(&tc, len); + let expected: Vec = lhs.iter().zip(&rhs).map(|(&l, &r)| l && r).collect(); + let mask = Mask::from_intersection_indices( + len, + model_indices(&lhs).into_iter(), + model_indices(&rhs).into_iter(), ); - assert_eq!(limited_mask.true_count(), 3); - - let original_mask = Mask::from_iter([true, true, false, true, false, true]); - let limited_mask = original_mask.clone().limit(100); - - assert_eq!(original_mask, limited_mask); + assert_matches_model(&mask, &expected); +} + +#[hegel::test] +fn mask_valid_counts(tc: TestCase) { + let (mask, bits) = draw_mask(&tc); + let mut indices: Vec = + tc.draw(gs::vecs(gs::integers::().max_value(bits.len())).max_size(MAX_LEN)); + indices.sort_unstable(); + let expected: Vec = indices + .iter() + .map(|&idx| bits[..idx].iter().filter(|&&b| b).count()) + .collect(); + assert_eq!(mask.valid_counts_for_indices(&indices), expected); +} + +#[hegel::test] +fn mask_intersect_by_rank(tc: TestCase) { + let (mask, bits) = draw_mask(&tc); + let (rank, rank_bits) = draw_mask_of_len(&tc, mask.true_count()); + let mut ranks = rank_bits.iter(); + let expected: Vec = bits + .iter() + .map(|&b| b && *ranks.next().unwrap_or(&false)) + .collect(); + assert_matches_model(&mask.intersect_by_rank(&rank), &expected); } -#[test] -fn test_limit_all_false_mask() { - let all_false = Mask::new_false(10); - let limited = all_false.clone().limit(5); - assert_eq!(limited, all_false); - assert!(limited.all_false()); -} +#[rstest] +#[case::all_true(Mask::new_true(4), vec![true, true, true, true])] +#[case::all_false(Mask::new_false(3), vec![false, false, false])] +#[case::values( + Mask::from_buffer(BitBuffer::from_iter([true, false, true, true, false])), + vec![true, false, true, true, false] +)] +#[case::empty(Mask::new_true(0), vec![])] +fn mask_iter_matches_value(#[case] mask: Mask, #[case] expected: Vec) { + let collected: Vec = mask.iter().collect(); + assert_eq!(collected, expected); -#[test] -fn test_limit_mask_exact() { - let mask = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let limited = mask.clone().limit(3); - assert_eq!(limited.true_count(), 3); - assert_eq!(limited, mask); -} + let by_value: Vec = (0..mask.len()).map(|i| mask.value(i)).collect(); + assert_eq!(collected, by_value); -#[test] -fn test_limit_mask_zero() { - let mask = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let limited = mask.limit(0); - assert!(limited.all_false()); - assert_eq!(limited.true_count(), 0); + let mut it = mask.iter(); + assert_eq!(it.len(), mask.len()); + if it.next().is_some() { + assert_eq!(it.len(), mask.len() - 1); + } } -// Buffer conversion tests #[test] -fn test_mask_to_bit_buffer() { - let all_true = Mask::new_true(5); - let buffer = all_true.to_bit_buffer(); - assert_eq!(buffer.true_count(), 5); - assert_eq!(buffer.len(), 5); - - let all_false = Mask::new_false(5); - let buffer = all_false.to_bit_buffer(); - assert_eq!(buffer.true_count(), 0); - assert_eq!(buffer.len(), 5); - - let values = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let buffer = values.to_bit_buffer(); - assert_eq!(buffer.true_count(), 3); - assert_eq!(buffer.len(), 5); +#[should_panic] +fn test_mask_slice_out_of_bounds() { + let mask = Mask::new_true(5); + mask.slice(3..8); // offset + length > len } -// MaskValues tests #[test] -fn test_mask_values() { - let all_true = Mask::new_true(5); - assert!(all_true.values().is_none()); - - let all_false = Mask::new_false(5); - assert!(all_false.values().is_none()); - - let mask = Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])); - let values = mask.values().unwrap(); - assert_eq!(values.len(), 5); - assert_eq!(values.true_count(), 3); - assert!(values.value(0)); - assert!(!values.value(1)); +#[should_panic(expected = "Masks must have the same length")] +fn test_bitand_different_lengths() { + let mask1 = Mask::new_true(5); + let mask2 = Mask::new_true(3); + let _unused = &mask1 & &mask2; } #[test] @@ -330,100 +407,6 @@ fn mask_from_buffer_with_slices_unset_bit() { ); } -#[test] -fn test_mask_values_is_empty() { - let empty_mask = Mask::from_buffer(BitBuffer::new_unset(0)); - assert!(empty_mask.values().is_none()); - - let non_empty_mask = Mask::from_buffer(BitBuffer::from_iter([true, false])); - assert!(!non_empty_mask.values().unwrap().is_empty()); -} - -// Creation from excluded indices -#[test] -fn test_mask_from_excluded_indices() { - let mask = Mask::from_excluded_indices(5, vec![1, 3]); - assert_eq!(mask.len(), 5); - assert_eq!(mask.true_count(), 3); - assert!(mask.value(0)); - assert!(!mask.value(1)); - assert!(mask.value(2)); - assert!(!mask.value(3)); - assert!(mask.value(4)); - - let mask_empty = Mask::from_excluded_indices(5, vec![]); - assert!(mask_empty.all_true()); - // Verify it returns the optimized AllTrue variant - assert!(matches!(mask_empty, Mask::AllTrue(5))); - - let mask_all = Mask::from_excluded_indices(3, vec![0, 1, 2]); - assert!(mask_all.all_false()); - // Verify it returns the optimized AllFalse variant - assert!(matches!(mask_all, Mask::AllFalse(3))); -} - -// Intersection tests -#[test] -fn test_mask_from_intersection_indices() { - let lhs = vec![0, 2, 3, 5, 7, 9]; - let rhs = vec![1, 2, 3, 4, 7, 8, 9]; - - let mask = Mask::from_intersection_indices(10, lhs.into_iter(), rhs.into_iter()); - assert_eq!(mask.len(), 10); - assert_eq!(mask.true_count(), 4); // 2, 3, 7, 9 - assert!(!mask.value(0)); - assert!(!mask.value(1)); - assert!(mask.value(2)); - assert!(mask.value(3)); - assert!(!mask.value(4)); - assert!(!mask.value(5)); - assert!(!mask.value(6)); - assert!(mask.value(7)); - assert!(!mask.value(8)); - assert!(mask.value(9)); -} - -#[test] -fn test_mask_from_intersection_indices_empty() { - let lhs = vec![0, 2, 4]; - let rhs = vec![1, 3, 5]; - - let mask = Mask::from_intersection_indices(6, lhs.into_iter(), rhs.into_iter()); - assert!(mask.all_false()); - assert_eq!(mask.true_count(), 0); -} - -#[test] -fn test_mask_from_intersection_indices_same() { - let indices = vec![1, 3, 5, 7]; - - let mask = - Mask::from_intersection_indices(10, indices.clone().into_iter(), indices.into_iter()); - assert_eq!(mask.len(), 10); - assert_eq!(mask.true_count(), 4); - assert!(mask.value(1)); - assert!(mask.value(3)); - assert!(mask.value(5)); - assert!(mask.value(7)); -} - -// Valid counts tests -#[test] -fn test_mask_valid_counts_for_indices() { - let mask = Mask::from_buffer(BitBuffer::from_iter([true, false, true, true, false, true])); - let indices = vec![0, 2, 4, 6]; - let counts = mask.valid_counts_for_indices(&indices); - assert_eq!(counts, vec![0, 1, 3, 4]); - - let all_true = Mask::new_true(6); - let counts = all_true.valid_counts_for_indices(&indices); - assert_eq!(counts, vec![0, 2, 4, 6]); - - let all_false = Mask::new_false(6); - let counts = all_false.valid_counts_for_indices(&indices); - assert_eq!(counts, vec![0, 0, 0, 0]); -} - #[test] #[should_panic] fn test_mask_valid_counts_for_indices_error() { @@ -432,67 +415,6 @@ fn test_mask_valid_counts_for_indices_error() { mask.valid_counts_for_indices(&indices); } -// FromIterator tests -#[test] -fn test_mask_from_iter_masks() { - let masks = vec![ - Mask::from_buffer(BitBuffer::from_iter([true, false])), - Mask::from_buffer(BitBuffer::from_iter([true, true, false])), - Mask::from_buffer(BitBuffer::from_iter([false, true])), - ]; - - let combined = Mask::from_iter(masks); - assert_eq!(combined.len(), 7); - assert_eq!(combined.true_count(), 4); - assert!(combined.value(0)); - assert!(!combined.value(1)); - assert!(combined.value(2)); - assert!(combined.value(3)); - assert!(!combined.value(4)); - assert!(!combined.value(5)); - assert!(combined.value(6)); -} - -#[test] -fn test_mask_from_iter_all_true() { - let masks = vec![Mask::new_true(3), Mask::new_true(2), Mask::new_true(4)]; - - let combined = Mask::from_iter(masks); - assert!(combined.all_true()); - assert_eq!(combined.len(), 9); -} - -#[test] -fn test_mask_from_iter_all_false() { - let masks = vec![Mask::new_false(3), Mask::new_false(2), Mask::new_false(4)]; - - let combined = Mask::from_iter(masks); - assert!(combined.all_false()); - assert_eq!(combined.len(), 9); -} - -#[test] -fn test_mask_from_iter_empty() { - let masks: Vec = vec![]; - let combined = Mask::from_iter(masks); - assert_eq!(combined.len(), 0); - assert!(combined.is_empty()); -} - -#[test] -fn test_mask_from_iter_with_empty_masks() { - let masks = vec![ - Mask::new_true(3), - Mask::new_true(0), // empty mask - Mask::new_false(2), - ]; - - let combined = Mask::from_iter(masks); - assert_eq!(combined.len(), 5); - assert_eq!(combined.true_count(), 3); -} - -// Panic tests for invalid inputs #[test] #[should_panic] fn test_mask_from_indices_unsorted() { @@ -535,7 +457,6 @@ fn test_mask_from_slices_overlapping() { Mask::from_slices(5, vec![(0, 3), (2, 4)]); // Overlapping ranges } -// Threshold iterator tests #[test] fn test_mask_threshold_iter() { let all_true = Mask::new_true(5); @@ -552,7 +473,6 @@ fn test_mask_threshold_iter() { } } -// Caching tests #[test] fn test_mask_indices_caching() { // Test that indices are properly cached @@ -595,7 +515,6 @@ fn test_mask_slices_caching() { } } -// AllOr tests #[test] fn test_allor_unwrap_or_else() { let all: AllOr = AllOr::All; @@ -645,165 +564,3 @@ fn test_allor_eq() { assert_ne!(all1, some1); assert_ne!(none1, some1); } - -// Parameterized tests with rstest -#[rstest] -#[case::all_true(Mask::new_true(5), 5, 5, 0, 1.0)] -#[case::all_false(Mask::new_false(5), 5, 0, 5, 0.0)] -#[case::mixed( - Mask::from_buffer(BitBuffer::from_iter([true, false, true, false, true])), - 5, 3, 2, 0.6 - )] -#[case::single_true(Mask::from_indices(10, vec![5]), 10, 1, 9, 0.1)] -#[case::dense(Mask::from_indices(10, vec![0, 1, 2, 3, 4, 5, 6, 7, 8]), 10, 9, 1, 0.9)] -fn test_mask_properties( - #[case] mask: Mask, - #[case] expected_len: usize, - #[case] expected_true: usize, - #[case] expected_false: usize, - #[case] expected_density: f64, -) { - assert_eq!(mask.len(), expected_len); - assert_eq!(mask.true_count(), expected_true); - assert_eq!(mask.false_count(), expected_false); - assert!((mask.density() - expected_density).abs() < 1e-10); -} - -#[rstest] -#[case::indices(vec![0, 2, 4], vec![(0, 1), (2, 3), (4, 5)])] -#[case::consecutive(vec![0, 1, 2], vec![(0, 3)])] -#[case::gap(vec![0, 1, 4, 5], vec![(0, 2), (4, 6)])] -#[case::single(vec![3], vec![(3, 4)])] -fn test_indices_to_slices_conversion( - #[case] indices: Vec, - #[case] expected_slices: Vec<(usize, usize)>, -) { - let mask = Mask::from_indices(10, indices.clone()); - - // Check indices - if let AllOr::Some(actual_indices) = mask.indices() { - assert_eq!(actual_indices, &indices[..]); - } else { - panic!("Expected Some variant for indices"); - } - - // Check slices - if let AllOr::Some(actual_slices) = mask.slices() { - assert_eq!(actual_slices, &expected_slices[..]); - } else { - panic!("Expected Some variant for slices"); - } -} - -#[rstest] -#[case::empty_intersection(vec![0, 2, 4], vec![1, 3, 5], vec![])] -#[case::full_intersection(vec![1, 3, 5], vec![1, 3, 5], vec![1, 3, 5])] -#[case::partial_intersection(vec![0, 1, 2, 3], vec![2, 3, 4, 5], vec![2, 3])] -#[case::subset_left(vec![1, 2], vec![0, 1, 2, 3], vec![1, 2])] -#[case::subset_right(vec![0, 1, 2, 3], vec![1, 2], vec![1, 2])] -fn test_intersection_indices( - #[case] left: Vec, - #[case] right: Vec, - #[case] expected: Vec, -) { - let mask = Mask::from_intersection_indices(10, left.into_iter(), right.into_iter()); - - match mask.indices() { - AllOr::Some(indices) if expected.is_empty() => assert!(indices.is_empty()), - AllOr::Some(indices) => assert_eq!(indices, &expected[..]), - AllOr::None if expected.is_empty() => {} - AllOr::None | AllOr::All => panic!("Unexpected result for intersection"), - } -} - -// Concat operation tests -#[test] -fn test_mask_concat_empty() { - let masks: Vec = vec![]; - let result = Mask::concat(masks.iter()).unwrap(); - assert_eq!(result.len(), 0); - assert!(result.is_empty()); -} - -#[test] -fn test_mask_concat_all_true() { - let masks = [Mask::new_true(3), Mask::new_true(2)]; - let result = Mask::concat(masks.iter()).unwrap(); - assert_eq!(result.len(), 5); - assert!(result.all_true()); -} - -#[test] -fn test_mask_concat_all_false() { - let masks = [Mask::new_false(3), Mask::new_false(2)]; - let result = Mask::concat(masks.iter()).unwrap(); - assert_eq!(result.len(), 5); - assert!(result.all_false()); -} - -#[test] -fn test_mask_concat_mixed_types() { - let masks = [ - Mask::from_buffer(BitBuffer::from_iter([true, false, true])), - Mask::new_true(2), - Mask::new_false(3), - ]; - - let result = Mask::concat(masks.iter()).unwrap(); - assert_eq!(result.len(), 8); - assert_eq!(result.true_count(), 4); - - // Verify the concatenated values - assert!(result.value(0)); // from buffer - assert!(!result.value(1)); // from buffer - assert!(result.value(2)); // from buffer - assert!(result.value(3)); // from all_true - assert!(result.value(4)); // from all_true - assert!(!result.value(5)); // from all_false - assert!(!result.value(6)); // from all_false - assert!(!result.value(7)); // from all_false -} - -// `Mask::iter` per-element bool iteration - -#[rstest] -#[case::all_true(Mask::new_true(4), vec![true, true, true, true])] -#[case::all_false(Mask::new_false(3), vec![false, false, false])] -#[case::values( - Mask::from_buffer(BitBuffer::from_iter([true, false, true, true, false])), - vec![true, false, true, true, false] -)] -#[case::empty(Mask::new_true(0), vec![])] -fn mask_iter_matches_value(#[case] mask: Mask, #[case] expected: Vec) { - // Iterator yields exactly one bool per element, matching `value`. - let collected: Vec = mask.iter().collect(); - assert_eq!(collected, expected); - - let by_value: Vec = (0..mask.len()).map(|i| mask.value(i)).collect(); - assert_eq!(collected, by_value); - - // ExactSizeIterator reports the right length, including after partial consumption. - let mut it = mask.iter(); - assert_eq!(it.len(), mask.len()); - if it.next().is_some() { - assert_eq!(it.len(), mask.len() - 1); - } -} - -/// `from_excluded_indices` must count each excluded position once: `[0, 0]` excludes a single row. -#[rstest] -#[case::fewer_than_len(vec![0, 0], vec![1, 2])] -#[case::equal_to_len(vec![0, 0, 0], vec![1, 2])] -#[case::more_than_len(vec![0, 0, 0, 0], vec![1, 2])] -#[case::non_adjacent(vec![2, 0, 2], vec![1])] -#[case::all_excluded(vec![2, 0, 2, 1], vec![])] -fn from_excluded_indices_counts_duplicates_once( - #[case] excluded: Vec, - #[case] included: Vec, -) { - let mask = Mask::from_excluded_indices(3, excluded); - assert_eq!(mask.true_count(), included.len()); - let expected = Mask::from_indices(3, included); - assert_eq!(mask, expected); - assert_eq!(mask.density(), expected.density()); -} From d2bb7fa257206e4b6a4fae4cb326388e0644aa8a Mon Sep 17 00:00:00 2001 From: Will Manning Date: Wed, 4 Nov 2026 12:04:52 -0500 Subject: [PATCH 26/41] Explorer: draw it with the Vortex design system, and build it with Bun (#11193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR draws the Explorer (vortex-web) with the Vortex design system and builds it with Bun. - The styles are copies of the system's CSS, plus `explorer.css` for the Explorer's own parts. Dtype colours come from the system. Encodings have no colour. - Blue marks only the selection. The header has links to the site, the docs, and bench. - Bytes use decimal units (1 KB = 1,000 bytes). The theme key is now `vx-theme`, and the old key moves over. - When a pane does not load, it says what did not load and what to do. The reason follows in a reference. - The PR removes parts that nothing used: LayoutSwimlane, AxisBar, SplitRegion, DtypeLegend, FileMap, and their styles. Not run: `bun run wasm`. Paired before and after screenshots will follow in a comment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Will Manning --- .github/workflows/web.yml | 28 +- REUSE.toml | 7 + renovate.json | 4 +- vortex-web/.prettierrc.json | 7 - vortex-web/.storybook/main.ts | 2 + vortex-web/.storybook/preview.ts | 11 +- vortex-web/README.md | 41 +- vortex-web/biome.jsonc | 80 + vortex-web/bun.lock | 654 ++ vortex-web/bunfig.toml | 12 + vortex-web/eslint.config.ts | 31 - vortex-web/index.html | 6 +- vortex-web/package-lock.json | 6260 ----------------- vortex-web/package.json | 59 +- vortex-web/public/theme.js | 19 + vortex-web/public/vortex_wordmark.svg | 72 + .../public/vortex_wordmark_dark_theme.svg | 72 + vortex-web/src/App.tsx | 14 +- vortex-web/src/components/DataTable.tsx | 437 +- vortex-web/src/components/Icon.tsx | 36 + vortex-web/src/components/Switch.tsx | 30 + vortex-web/src/components/ThemePicker.tsx | 105 +- .../components/detail/ArraySummaryPane.tsx | 42 +- .../src/components/detail/BuffersPane.tsx | 156 +- .../components/detail/DetailPanel.stories.tsx | 4 +- .../src/components/detail/DetailPanel.tsx | 225 +- .../src/components/detail/EncodingPane.tsx | 25 +- vortex-web/src/components/detail/Facts.tsx | 24 + .../src/components/detail/SegmentsPane.tsx | 29 +- .../src/components/detail/SummaryPane.tsx | 90 +- .../src/components/detail/SummarySidebar.tsx | 17 +- .../explorer/BlockTreemap.stories.tsx | 1 - .../src/components/explorer/BlockTreemap.tsx | 179 +- .../src/components/explorer/DataPreview.tsx | 64 +- .../explorer/ExplorerShell.stories.tsx | 2 +- .../components/explorer/FileDropScreen.tsx | 198 +- .../src/components/explorer/FileHeader.tsx | 100 +- .../components/explorer/FileMap.stories.tsx | 41 - .../src/components/explorer/FileMap.tsx | 322 - vortex-web/src/components/explorer/Header.tsx | 86 + .../src/components/explorer/MainArea.tsx | 112 +- .../src/components/explorer/StatusBar.tsx | 68 +- .../src/components/explorer/TreePanel.tsx | 186 +- .../src/components/explorer/physicalStats.ts | 2 +- .../src/components/swimlane/AxisBar.tsx | 64 - .../src/components/swimlane/DtypeLegend.tsx | 17 - .../swimlane/LayoutSwimlane.stories.tsx | 130 - .../components/swimlane/LayoutSwimlane.tsx | 274 - .../src/components/swimlane/SplitRegion.tsx | 59 - .../src/components/swimlane/SwimlaneBar.tsx | 82 +- .../components/swimlane/SwimlaneOverview.tsx | 159 +- .../src/components/swimlane/Tooltip.tsx | 59 +- .../src/components/swimlane/TreeRow.tsx | 145 +- .../src/components/swimlane/TreeSearch.tsx | 39 +- vortex-web/src/components/swimlane/index.ts | 50 - vortex-web/src/components/swimlane/styles.ts | 17 - vortex-web/src/components/swimlane/types.ts | 14 +- vortex-web/src/components/swimlane/utils.ts | 218 +- vortex-web/src/contexts/ThemeContext.tsx | 47 +- vortex-web/src/index.css | 95 +- vortex-web/src/mocks/layouts.ts | 2 +- vortex-web/src/styles/explorer.css | 779 ++ vortex-web/src/styles/vortex-base.css | 155 + vortex-web/src/styles/vortex-tokens.css | 207 + vortex-web/src/styles/vortex.css | 760 ++ vortex-web/src/workers/VortexWorker.ts | 2 +- vortex-web/tsconfig.app.json | 4 +- vortex-web/tsconfig.json | 5 +- vortex-web/tsconfig.node.json | 6 +- vortex-web/vite.config.ts | 7 +- 70 files changed, 4374 insertions(+), 8982 deletions(-) delete mode 100644 vortex-web/.prettierrc.json create mode 100644 vortex-web/biome.jsonc create mode 100644 vortex-web/bun.lock create mode 100644 vortex-web/bunfig.toml delete mode 100644 vortex-web/eslint.config.ts delete mode 100644 vortex-web/package-lock.json create mode 100644 vortex-web/public/theme.js create mode 100644 vortex-web/public/vortex_wordmark.svg create mode 100644 vortex-web/public/vortex_wordmark_dark_theme.svg create mode 100644 vortex-web/src/components/Icon.tsx create mode 100644 vortex-web/src/components/Switch.tsx create mode 100644 vortex-web/src/components/detail/Facts.tsx delete mode 100644 vortex-web/src/components/explorer/FileMap.stories.tsx delete mode 100644 vortex-web/src/components/explorer/FileMap.tsx create mode 100644 vortex-web/src/components/explorer/Header.tsx delete mode 100644 vortex-web/src/components/swimlane/AxisBar.tsx delete mode 100644 vortex-web/src/components/swimlane/DtypeLegend.tsx delete mode 100644 vortex-web/src/components/swimlane/LayoutSwimlane.stories.tsx delete mode 100644 vortex-web/src/components/swimlane/LayoutSwimlane.tsx delete mode 100644 vortex-web/src/components/swimlane/SplitRegion.tsx delete mode 100644 vortex-web/src/components/swimlane/index.ts delete mode 100644 vortex-web/src/components/swimlane/styles.ts create mode 100644 vortex-web/src/styles/explorer.css create mode 100644 vortex-web/src/styles/vortex-base.css create mode 100644 vortex-web/src/styles/vortex-tokens.css create mode 100644 vortex-web/src/styles/vortex.css diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 80afd71c1e5..65ddc32cb33 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -56,17 +56,19 @@ jobs: uses: taiki-e/install-action@1e29709c0ec63babbe8f266397a465f29a825f91 # v2 with: tool: wasm-pack@0.15.0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.4.3 + # Storybook and Vite run on Node, and Bun runs their scripts with it. - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: "24" - cache: "npm" - cache-dependency-path: vortex-web/package-lock.json - - run: npm ci - - run: npm run wasm - - run: npm run format:check - - run: npm run lint - - run: npm run typecheck - - run: npm run build-storybook + - run: bun install --frozen-lockfile + - run: bun run wasm + - run: bun run format:check + - run: bun run lint + - run: bun run typecheck + - run: bun run build-storybook build: name: Build @@ -83,13 +85,15 @@ jobs: uses: taiki-e/install-action@1e29709c0ec63babbe8f266397a465f29a825f91 # v2 with: tool: wasm-pack@0.15.0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.4.3 + # Storybook and Vite run on Node, and Bun runs their scripts with it. - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: "24" - cache: "npm" - cache-dependency-path: vortex-web/package-lock.json - - run: npm ci - - run: npm run build + - run: bun install --frozen-lockfile + - run: bun run build - name: Upload build artifact uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: diff --git a/REUSE.toml b/REUSE.toml index d3639170e26..2d0a8185788 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -43,6 +43,13 @@ precedence = "override" SPDX-FileCopyrightText = "Copyright the Vortex contributors" SPDX-License-Identifier = "Apache-2.0" +# The Vortex design system's stylesheets, copied into the Explorer by the system's +# tools/site-styles.mjs. The copies carry the tool's header, not an SPDX one. +[[annotations]] +path = "vortex-web/src/styles/vortex*.css" +SPDX-FileCopyrightText = "Copyright the Vortex contributors" +SPDX-License-Identifier = "Apache-2.0" + [[annotations]] path = "vortex-ffi/cmake/tests/fixtures/native/vortex-ffi/header-version" SPDX-FileCopyrightText = "Copyright the Vortex contributors" diff --git a/renovate.json b/renovate.json index 90c50d9fd79..cc83c8c4149 100644 --- a/renovate.json +++ b/renovate.json @@ -133,7 +133,7 @@ "separateMultipleMajor": false }, { - "description": "Propose major updates for the explore app in vortex-web, all in one PR per run. Lock file maintenance already takes compatible releases, but without this a new major (Vite, React, Storybook) never arrives. Not automerged: a person reviews the group.", + "description": "Propose major updates for the explore app in vortex-web, all in one PR per run. Lock file maintenance already takes compatible releases, but without this a new major (Vite, React, Storybook) never arrives. Not automerged: a person reviews the group. vortex-web installs with Bun; Renovate's npm manager also reads and updates bun.lock.", "matchManagers": [ "npm" ], @@ -171,7 +171,7 @@ ] }, { - "description": "Renovate passes the 7-day wait to npm as --before. The top-level npmrc sets allow-remote=all only for Renovate's npm runs: npm 12 refuses some registry tarballs of packages with bundleDependencies (npm/cli#9800), including one in vortex-web. Remove that npmrc when the bug is fixed.", + "description": "Renovate passes the 7-day wait to npm as --before, for .github/vercel-cli. It cannot pass the wait to Bun, so for vortex-web the minimumReleaseAge in vortex-web/bunfig.toml enforces it. The top-level npmrc sets allow-remote=all only for Renovate's npm runs: npm 12 refuses some registry tarballs of packages with bundleDependencies (npm/cli#9800). vortex-web had one but now installs with Bun, and the .github/vercel-cli tree has none today. The npmrc stays in case a Vercel CLI update brings one in. Remove it when the bug is fixed.", "groupName": "JS lock file maintenance", "groupSlug": "js-lock-file-maintenance", "matchManagers": [ diff --git a/vortex-web/.prettierrc.json b/vortex-web/.prettierrc.json deleted file mode 100644 index 50bf5f7ae21..00000000000 --- a/vortex-web/.prettierrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "singleQuote": true, - "trailingComma": "all", - "bracketSpacing": true, - "printWidth": 100, - "tabWidth": 2 -} diff --git a/vortex-web/.storybook/main.ts b/vortex-web/.storybook/main.ts index 169c5bb6be7..5a20005d32e 100644 --- a/vortex-web/.storybook/main.ts +++ b/vortex-web/.storybook/main.ts @@ -7,5 +7,7 @@ const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: ['@storybook/addon-docs'], framework: '@storybook/react-vite', + // The app's public files, so components that load /vortex_wordmark.svg find it, as in the app. + staticDirs: ['../public'], }; export default config; diff --git a/vortex-web/.storybook/preview.ts b/vortex-web/.storybook/preview.ts index dbb0795b0fc..0b3389efd0c 100644 --- a/vortex-web/.storybook/preview.ts +++ b/vortex-web/.storybook/preview.ts @@ -16,13 +16,14 @@ const preview: Preview = { items: [ { value: 'light', title: 'Light', icon: 'sun' }, { value: 'dark', title: 'Dark', icon: 'moon' }, + { value: 'system', title: 'System', icon: 'mirror' }, ], dynamicTitle: true, }, }, }, initialGlobals: { - theme: 'light', + theme: 'dark', }, decorators: [ (Story, context) => { @@ -30,8 +31,12 @@ const preview: Preview = { // (e.g. FileHeader/ThemePicker, which call useTheme) have their context. // Seed it from the toolbar's theme global; `key` remounts the provider // when the toolbar theme changes so the switch takes effect. - const theme = (context.globals.theme as string) || 'light'; - localStorage.setItem('vortex-theme', theme); + const theme = (context.globals.theme as string) || 'dark'; + try { + localStorage.setItem('vx-theme', theme); + } catch { + // Storage can be blocked; the provider then starts from its default. + } return createElement(ThemeProvider, { key: theme }, Story()); }, ], diff --git a/vortex-web/README.md b/vortex-web/README.md index bbcb54c3f74..ee5e927a016 100644 --- a/vortex-web/README.md +++ b/vortex-web/README.md @@ -1,24 +1,27 @@ # Vortex Web -A web UI for exploring Vortex data files, built with React, TypeScript, Tailwind CSS, and Rust/WASM. +A web UI for exploring Vortex data files, built with React, TypeScript, and Rust/WASM, and drawn with the Vortex design system. ## Prerequisites -- Node.js 22+ +- [Bun](https://bun.com) 1.4.3, the version in `package.json`'s `packageManager` field +- Node.js 24+ (Storybook and Vite run on Node) - [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) (for full app development) - Rust toolchain (for full app development) ## Getting Started ```bash -npm install +bun install ``` +`bunfig.toml` makes Bun refuse package versions younger than 7 days. + ### Full App (requires Rust + wasm-pack) ```bash # Start dev server (builds WASM in debug mode, then starts Vite) -npm run dev +bun run dev ``` ### Storybook (no Rust/WASM required) @@ -26,7 +29,7 @@ npm run dev Storybook lets you develop and preview UI components in isolation: ```bash -npm run storybook +bun run storybook ``` This starts a dev server at http://localhost:6006. @@ -35,14 +38,25 @@ This starts a dev server at http://localhost:6006. | Command | Description | | ------------------------- | ------------------------------------------ | -| `npm run dev` | Build WASM (debug) + start Vite dev server | -| `npm run build` | Production build (WASM release + Vite) | -| `npm run storybook` | Start Storybook dev server on port 6006 | -| `npm run build-storybook` | Build static Storybook site | -| `npm run lint` | Run ESLint | -| `npm run lint:fix` | Run ESLint with auto-fix | -| `npm run typecheck` | Run TypeScript type checking | -| `npm run check` | Build WASM + lint + typecheck | +| `bun run dev` | Build WASM (debug) + start Vite dev server | +| `bun run build` | Production build (WASM release + Vite) | +| `bun run storybook` | Start Storybook dev server on port 6006 | +| `bun run build-storybook` | Build static Storybook site | +| `bun run lint` | Lint with Biome | +| `bun run lint:fix` | Lint with Biome and apply safe fixes | +| `bun run format` | Format with Biome | +| `bun run format:check` | Check formatting with Biome | +| `bun run typecheck` | Run TypeScript type checking | +| `bun run check` | Build WASM + Biome check + typecheck | + +## Styles + +The Explorer uses the Vortex design system's CSS and no CSS framework. + +- `src/styles/vortex-tokens.css`, `vortex-base.css`, and `vortex.css` are copies of the design system's styles. Change the system, then copy them again; do not edit the copies. +- `src/styles/explorer.css` lays out the Explorer on top of the system. Its classes start with `x-`; the system's start with `vx-`. +- Colours come from the system's tokens, so every view follows the theme. A dtype's colour comes from its `vx-dt-*` class (`dtypeClass` in `components/swimlane/utils.ts`). An encoding has no colour; the views name it. +- The production CSP allows no inline script, so `public/theme.js` sets the theme before the first paint. ## Writing Stories @@ -70,6 +84,7 @@ export const Default: Story = { vortex-web/ crate/ # Rust WASM crate (vortex bindings) src/ # React/TypeScript frontend + styles/ # The design system's CSS, and the Explorer's own wasm/pkg/ # Generated WASM bindings (not checked in) .storybook/ # Storybook configuration public/ # Static assets diff --git a/vortex-web/biome.jsonc b/vortex-web/biome.jsonc new file mode 100644 index 00000000000..8f3569b2ad8 --- /dev/null +++ b/vortex-web/biome.jsonc @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + // src/styles/vortex*.css are copies of the Vortex design system's + // stylesheets: change the system, not the copies. + "includes": ["**", "!bun.lock", "!crate", "!public", "!src/styles/vortex*.css"] + }, + // The style that .prettierrc.json set before the move to Biome. + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSpacing": true + } + }, + "css": { + "formatter": { + "quoteStyle": "single" + } + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended", + "correctness": { + "useExhaustiveDependencies": { + "level": "error", + "options": { + // Some effects list a value only to run again when it changes, as + // eslint-plugin-react-hooks allowed. + "reportUnnecessaryDependencies": false + } + }, + "useHookAtTopLevel": "error" + }, + "style": { + // A ! marks a ref set in an effect, the root element, or an index + // that noUncheckedIndexedAccess cannot prove is in range. + "noNonNullAssertion": "off" + }, + "suspicious": { + // The lists keyed by index are positional (histogram bins, byte + // columns, buffer numbers), so the index is the identity. + "noArrayIndexKey": "off" + }, + "a11y": { + // Every pointer control needs a keyboard path. + "noStaticElementInteractions": "error", + "useKeyWithClickEvents": "error" + } + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "off" + } + } + } +} diff --git a/vortex-web/bun.lock b/vortex-web/bun.lock new file mode 100644 index 00000000000..02a195e2803 --- /dev/null +++ b/vortex-web/bun.lock @@ -0,0 +1,654 @@ +{ + "lockfileVersion": 2, + "configVersion": 0, + "workspaces": { + "": { + "name": "vortex-web", + "dependencies": { + "@tanstack/react-table": "^9.3.1", + "@tanstack/react-virtual": "^3.14.23", + "apache-arrow": "^21.2.0", + "d3-hierarchy": "^3.1.2", + "react": "^19.3.1", + "react-dom": "^19.3.1", + }, + "devDependencies": { + "@biomejs/biome": "^2.6.2", + "@storybook/addon-docs": "^10.6.5", + "@storybook/react-vite": "^10.6.5", + "@types/d3-hierarchy": "^3.1.7", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "storybook": "^10.6.5", + "typescript": "^7.0.2", + "vite": "^8.3.5", + }, + }, + }, + "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.11", "", {}, "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.10", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.8" } }, "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg=="], + + "@babel/parser": ["@babel/parser@7.29.11", "", { "dependencies": { "@babel/types": "^7.29.11" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag=="], + + "@babel/runtime": ["@babel/runtime@7.29.10", "", {}, "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.10", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.9", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ=="], + + "@babel/types": ["@babel/types@7.29.11", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.11", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ=="], + + "@biomejs/biome": ["@biomejs/biome@2.6.3", "", { "optionalDependencies": { "@biomejs/cli-android-arm64": "2.6.3", "@biomejs/cli-android-x64": "2.6.3", "@biomejs/cli-darwin-arm64": "2.6.3", "@biomejs/cli-darwin-x64": "2.6.3", "@biomejs/cli-linux-arm64": "2.6.3", "@biomejs/cli-linux-arm64-musl": "2.6.3", "@biomejs/cli-linux-x64": "2.6.3", "@biomejs/cli-linux-x64-musl": "2.6.3", "@biomejs/cli-win32-arm64": "2.6.3", "@biomejs/cli-win32-x64": "2.6.3" }, "bin": { "biome": "bin/biome" } }, "sha512-va/0xPku7wnfzv+D8Zpdm5uKiGJWiV8neqoEPnRjvNQQwhl4tVXS4fETs4H0CCB2rGeqJ4cu9y4iOZOnOYKAUA=="], + + "@biomejs/cli-android-arm64": ["@biomejs/cli-android-arm64@2.6.3", "", { "os": "android", "cpu": "arm64" }, "sha512-5JD7QQDEHxdeEb4sftEHG+lsuUyu8Cob/hVLqRELbgGlFl68Rldv/PPi2k4dRjERZ/EItJ3O6Xmv8lqsEtUBiw=="], + + "@biomejs/cli-android-x64": ["@biomejs/cli-android-x64@2.6.3", "", { "os": "android", "cpu": "x64" }, "sha512-70WOGKCgpT+Uqg6LFdTLqefV54/DypdZg+LnBQDO6AWvkgXO+9xhVClajQyDsIHYjnXMtI1pRO5sP+zMXlS/bw=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.6.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ezf+XEKeyJ2yYlBhrzyEEHLvfRYrhtNKc6maLzR/WcAcBccb4RVDeFxsymjBve2LO0bpIMRUAiS90D4bUBRT0A=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.6.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Lg2tFdg4elJo+7i707PpLMs6N1hATVt4kqDo6MD/q/VnPs8rsKReKXsz1TA5GNdfRVAQNJ5GRiyK2EwNptK+MA=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.6.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8Stcvq98n1gs9VyLKuaWrnsd8kbXE2nKDAHtuLHGSYdGyNWN5JIIm5a28xdnmgdKzKHmiDgYyATetkEv7+zGow=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.6.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-dYU70A89OFHzakQPE9VOPbE60JNcQt4mtxvUGQl+EgTkGtqCFxLD2RUn1SbiMu8Mu5xFscS1bfYDpoasaUrqNA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.6.3", "", { "os": "linux", "cpu": "x64" }, "sha512-CShmx0bO6BngywT3minv8K1xsQX1ig1vsBqYTLnmDY3mBiDYHlaLRIsdHoM1pEU9dV3BN5a0uiH0IGDtYh9TWA=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.6.3", "", { "os": "linux", "cpu": "x64" }, "sha512-j6eFjUcWSphmgVTbrobEMA0wdbfNHKrGi8hRsFkc17jeS1n7nHwFACxp6jWqe9sxQqyFAeyOgt2/1bOGVcbATA=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.6.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-00SthDxBPwNiT7IL/mOneif6fiEU1PXK875zhr+InpeZjovY1mF5tCuDhXo1Hh0GYyiKHKzLbItzn58SAyLhJQ=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.6.3", "", { "os": "win32", "cpu": "x64" }, "sha512-GhtOPphBWlnCepqpNk/w5eGN5oGRCzL20EbAkL1XCIpyboeVC9R642noyT43l8LSenGS4qz9IL8sKPfwY37V/Q=="], + + "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], + + "@joshwooding/vite-plugin-react-docgen-typescript": ["@joshwooding/vite-plugin-react-docgen-typescript@0.7.0", "", { "dependencies": { "glob": "^13.0.1", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "typescript": ">= 4.3.x", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" } }, "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw=="], + + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.127.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.127.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.127.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA=="], + + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.127.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ=="], + + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.127.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg=="], + + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.127.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ=="], + + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.127.0", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.127.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw=="], + + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.127.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], + + "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.21.2", "", { "os": "android", "cpu": "arm" }, "sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ=="], + + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.21.2", "", { "os": "android", "cpu": "arm64" }, "sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.21.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.21.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.21.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.21.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A=="], + + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.21.2", "", { "os": "linux", "cpu": "arm" }, "sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.21.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.21.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q=="], + + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.21.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.21.2", "", { "os": "linux", "cpu": "none" }, "sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g=="], + + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.21.2", "", { "os": "linux", "cpu": "none" }, "sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.21.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.21.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.21.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og=="], + + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.21.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.21.2", "", { "dependencies": { "@emnapi/core": "1.11.0", "@emnapi/runtime": "1.11.0", "@napi-rs/wasm-runtime": "^1.1.5" }, "cpu": "none" }, "sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.21.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.21.2", "", { "os": "win32", "cpu": "x64" }, "sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.15", "", { "os": "android", "cpu": "arm" }, "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.15", "", { "os": "android", "cpu": "arm64" }, "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.15", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.15", "", { "os": "linux", "cpu": "arm" }, "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.15", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.15", "", { "os": "linux", "cpu": "s390x" }, "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.15", "", { "os": "none", "cpu": "arm64" }, "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.15", "", { "os": "win32", "cpu": "x64" }, "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.5.1", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-P9ZvnfouRu4MSC+ilERaw6FmZs8zqKhpHxEs4aGDGWQAMzQGXnhvCSvoNwokfNvdt7t3Y5Y4p6gStQgezeIqug=="], + + "@storybook/addon-docs": ["@storybook/addon-docs@10.6.5", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.6.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0", "unplugin": "^2.3.5" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.5" }, "optionalPeers": ["@types/react"] }, "sha512-Kt0pGLA+nwdu2g80p8jbdoiiKXxwQbhrY6uzMzq/0G1lytxpJUzrOQAM+GDcEbqyvpKr0u5d1q05/LPem/uooQ=="], + + "@storybook/builder-vite": ["@storybook/builder-vite@10.6.5", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.6.5", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-udrGjg3yt38pz4FxplypXxw1creIGS7N6QS8xfvTFM8MQNtPyojT/gZdd4fhX0dFrQx5MMuJEYJA5gUBiPsLtg=="], + + "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], + + "@storybook/icons": ["@storybook/icons@2.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-FpUB/xizIrV1ILhCIYFo+TtQNN9YEoLjfcnAW0JtOXL+BcM2aisSysI0B54JMWDnljriXySBHgN6ps4s9lb75A=="], + + "@storybook/react": ["@storybook/react@10.6.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "10.6.5", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.5", "typescript": ">= 4.9.x" }, "optionalPeers": ["@types/react", "@types/react-dom", "typescript"] }, "sha512-vvRjRe/O8NRiQbGyA7Ne6h2jbD6GjwNZFkMdHl+0kZH8Z/S9ZWWmKmjCySvH/OmgighdCOQ98tcfND7m65WQBg=="], + + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.6.5", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.5" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Qbbkp+5Otg32w8Ddav6rH6MGPiYVfEzrgn0HqbqlqS8kY+DRXo+NYXJwZTVSDSwrYPrfH+Ok2tV4uE8Zo0c3cg=="], + + "@storybook/react-vite": ["@storybook/react-vite@10.6.5", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@rollup/pluginutils": "^5.0.2", "@storybook/builder-vite": "10.6.5", "@storybook/react": "10.6.5", "empathic": "^2.0.0", "magic-string": "^1.1.0", "react-docgen": "^8.0.2", "resolve": "^1.22.8", "tsconfig-paths": "^4.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.6.5", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["typescript"] }, "sha512-RsRF1drYtCku7+PMdRnuFt3EdG8Rn8I5G8ne0cBjn24xSn5Pbo9W+y9IoyD2llAeaeCpzEEdASY12wpKd3aCQA=="], + + "@tanstack/react-store": ["@tanstack/react-store@0.11.2", "", { "dependencies": { "@tanstack/store": "0.11.2", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ=="], + + "@tanstack/react-table": ["@tanstack/react-table@9.3.1", "", { "dependencies": { "@tanstack/react-store": "^0.11.2", "@tanstack/table-core": "9.3.1" }, "peerDependencies": { "react": ">=18" } }, "sha512-E6Ya5TnY6EaGyZYIUqig/29Lr/gJSAubQkLJFqiWkUl5nRCr0Xr9hvJot+NiRTtG+TzGxHTiqUW3bnLtQxlHug=="], + + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.23", "", { "dependencies": { "@tanstack/virtual-core": "3.19.8" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Oyy58qQwdaQieA+A4WUDtnh9D1rv5FZvGdw8og9Xeyh88V6HH94vMSyV5i9ATIbYNe9t5Ix0WXRo9NOTpHDkYw=="], + + "@tanstack/store": ["@tanstack/store@0.11.2", "", {}, "sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q=="], + + "@tanstack/table-core": ["@tanstack/table-core@9.3.1", "", { "dependencies": { "@tanstack/store": "^0.11.2" } }, "sha512-zQvlxSNi2qGM3L4jh85Z6hqMfdIR5qOfD9KHOU32nuKFJGN0RlJ7UvV1lsPb1fPVhpFYI0VP3jJGxLdLDpTKjQ=="], + + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.19.8", "", {}, "sha512-Oz/SYBo/aVPyQkwtOw7Q23myITUgFIof2glQqydYJAPL6mH2SkJDkvDKZTklB+7gv5jGLZQhyTcd7SO1A1EODQ=="], + + "@testing-library/dom": ["@testing-library/dom@10.4.2", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.8", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/doctrine": ["@types/doctrine@0.0.9", "", {}, "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="], + + "@types/node": ["@types/node@25.9.9", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-b4e2xxj/yMeT2hNlD6x7zNFiQK2Dlmkb9CQ6v3/1G2RezKN4YS2lcEv8lzGm1mhs4f2lM4yNAMI/zUDBZBBt7w=="], + + "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="], + + "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="], + + "@types/resolve": ["@types/resolve@1.20.6", "", {}, "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.152.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw=="], + + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "@webcontainer/env": ["@webcontainer/env@1.1.1", "", {}, "sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng=="], + + "acorn": ["acorn@8.19.0", "", { "bin": "bin/acorn" }, "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "apache-arrow": ["apache-arrow@21.2.0", "", { "dependencies": { "@types/node": "^25.2.0", "flatbuffers": "^25.1.24", "json-with-bigint": "^3.5.3", "tslib": "^2.6.2" }, "bin": { "arrow2csv": "bin/arrow2csv.js" } }, "sha512-Hxe6Agq26gQOM954qpzYSllJBPJl+e16U5CkfuMUhLrNba+5nKkttIVlflaovN6oaTratqMGAO8H5u/aNhmHWQ=="], + + "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-types": ["ast-types@0.16.3", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.34", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA=="], + + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], + + "browserslist": ["browserslist@4.29.5", "", { "dependencies": { "baseline-browser-mapping": "^2.11.33", "caniuse-lite": "^1.0.30001819", "electron-to-chromium": "^1.5.464", "node-releases": "^2.0.60", "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ=="], + + "bundle-name": ["bundle-name@4.1.1", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001819", "", {}, "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.467", "", {}, "sha512-Nw9X371rtZmYcBSnEijnCDONILjs93biCZO34juC7MBOmK8E+GFnyD4T/WvyrlJkppul7oxg6TwvtKst9SODTw=="], + + "empathic": ["empathic@2.1.0", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": "bin/esbuild" }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": "cli.js" }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": "cli.js" }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-with-bigint": ["json-with-bigint@3.5.12", "", {}, "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w=="], + + "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": "bin/bin.js" }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.20", "", { "bin": "bin/nanoid.cjs" }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], + + "node-releases": ["node-releases@2.0.60", "", {}, "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ=="], + + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + + "oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="], + + "oxc-resolver": ["oxc-resolver@11.21.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.21.2", "@oxc-resolver/binding-android-arm64": "11.21.2", "@oxc-resolver/binding-darwin-arm64": "11.21.2", "@oxc-resolver/binding-darwin-x64": "11.21.2", "@oxc-resolver/binding-freebsd-x64": "11.21.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.21.2", "@oxc-resolver/binding-linux-arm64-musl": "11.21.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.21.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.21.2", "@oxc-resolver/binding-linux-x64-gnu": "11.21.2", "@oxc-resolver/binding-linux-x64-musl": "11.21.2", "@oxc-resolver/binding-openharmony-arm64": "11.21.2", "@oxc-resolver/binding-wasm32-wasi": "11.21.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.21.2", "@oxc-resolver/binding-win32-x64-msvc": "11.21.2" } }, "sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "postcss": ["postcss@8.5.32", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g=="], + + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "react": ["react@19.3.1", "", {}, "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw=="], + + "react-docgen": ["react-docgen@8.0.4", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.2", "@types/babel__core": "^7.20.5", "@types/babel__traverse": "^7.20.7", "@types/doctrine": "^0.0.9", "@types/resolve": "^1.20.2", "doctrine": "^3.0.0", "resolve": "^1.22.1", "strip-indent": "^4.0.0" } }, "sha512-gIYjtVTU4NCWZnb/LXVSWfClZXnjzX3+xxi69Tkfu4YsmYuqiOk8giLkKt/WwxWqh+U66CTh8Kqbha1uPv8/qQ=="], + + "react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="], + + "react-dom": ["react-dom@19.3.1", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.1" } }, "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "recast": ["recast@0.23.21", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "resolve": ["resolve@1.22.13", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.17.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA=="], + + "rolldown": ["rolldown@1.2.15", "", { "dependencies": { "@oxc-project/types": "=0.155.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.15", "@rolldown/binding-android-arm64": "1.2.15", "@rolldown/binding-darwin-arm64": "1.2.15", "@rolldown/binding-darwin-x64": "1.2.15", "@rolldown/binding-freebsd-x64": "1.2.15", "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", "@rolldown/binding-linux-arm64-gnu": "1.2.15", "@rolldown/binding-linux-arm64-musl": "1.2.15", "@rolldown/binding-linux-ppc64-gnu": "1.2.15", "@rolldown/binding-linux-s390x-gnu": "1.2.15", "@rolldown/binding-linux-x64-gnu": "1.2.15", "@rolldown/binding-linux-x64-musl": "1.2.15", "@rolldown/binding-openharmony-arm64": "1.2.15", "@rolldown/binding-win32-arm64-msvc": "1.2.15", "@rolldown/binding-win32-x64-msvc": "1.2.15" }, "bin": "bin/cli.mjs" }, "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], + + "semver": ["semver@7.8.5", "", { "bin": "bin/semver.js" }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], + + "storybook": ["storybook@10.6.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "^14.6.3", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "11.21.2", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.21.1" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15 || ^0.2.0" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-qurC38Nx4PDF9YIq6GO95dpUx8a3B9AZwlcHBo4cOI9At09ifjQRsqcwcdOf2YX0cpVhFKmHkI20SWCrxqYQrg=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.6", "", {}, "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg=="], + + "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], + + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "update-browserslist-db": ["update-browserslist-db@1.3.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A=="], + + "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], + + "vite": ["vite@8.3.5", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.30", "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ=="], + + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], + + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], + + "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], + + "@testing-library/jest-dom/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + + "redent/strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "rolldown/@oxc-project/types": ["@oxc-project/types@0.155.0", "", {}, "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q=="], + + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + } +} diff --git a/vortex-web/bunfig.toml b/vortex-web/bunfig.toml new file mode 100644 index 00000000000..bb587ea7ffe --- /dev/null +++ b/vortex-web/bunfig.toml @@ -0,0 +1,12 @@ +# Bun configuration for vortex-web. Bun merges it with $HOME/.bunfig.toml. + +[install] +# Link only the dependencies each package declares. No code can load a +# transitive dependency that its own package does not declare. +linker = "isolated" + +# Refuse package versions younger than 7 days (in seconds). npm worms are +# usually found and removed upstream within hours, so a 7-day wait keeps +# them out of a local install, a lock file refresh, and CI. Renovate waits +# the same 7 days before it proposes an update (renovate.json). +minimumReleaseAge = 604800 diff --git a/vortex-web/eslint.config.ts b/vortex-web/eslint.config.ts deleted file mode 100644 index 8c6a8e00920..00000000000 --- a/vortex-web/eslint.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -import js from "@eslint/js"; -import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; -import prettier from "eslint-config-prettier"; -import storybook from "eslint-plugin-storybook"; -import tseslint from "typescript-eslint"; - -export default tseslint.config({ ignores: ["dist", "storybook-static"] }, { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ["**/*.{ts,tsx}"], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, - }, - rules: { - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], - }, -}, storybook.configs["flat/recommended"], prettier); diff --git a/vortex-web/index.html b/vortex-web/index.html index 6ed92bb0d33..0245446fe07 100644 --- a/vortex-web/index.html +++ b/vortex-web/index.html @@ -2,17 +2,19 @@ - + Vortex Explorer + + diff --git a/vortex-web/package-lock.json b/vortex-web/package-lock.json deleted file mode 100644 index 28b98951354..00000000000 --- a/vortex-web/package-lock.json +++ /dev/null @@ -1,6260 +0,0 @@ -{ - "name": "vortex-web", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "vortex-web", - "version": "0.1.0", - "dependencies": { - "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.23", - "apache-arrow": "^21.1.0", - "d3-hierarchy": "^3.1.2", - "react": "^19.1.0", - "react-dom": "^19.1.0" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@storybook/addon-docs": "^10.3.5", - "@storybook/react-vite": "^10.3.5", - "@tailwindcss/vite": "^4.2.4", - "@types/d3-hierarchy": "^3.1.7", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^10.2.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "eslint-plugin-storybook": "^10.3.5", - "globals": "^17.5.0", - "prettier": "^3.8.3", - "storybook": "^10.3.5", - "tailwindcss": "^4.2.4", - "typescript": "~6.0.0", - "typescript-eslint": "^8.59.0", - "vite": "^8.0.10" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", - "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", - "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", - "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", - "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.11" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", - "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", - "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.9", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", - "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.11", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cacheable/memory": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", - "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cacheable/utils": "^2.5.0", - "@keyv/bigmap": "^1.3.1", - "hookified": "^1.15.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@cacheable/utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", - "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hashery": "^1.5.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", - "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", - "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.7.0.tgz", - "integrity": "sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^13.0.1", - "react-docgen-typescript": "^2.2.2" - }, - "peerDependencies": { - "typescript": ">= 4.3.x", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@mdx-js/react": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", - "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdx": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz", - "integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", - "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" - } - }, - "node_modules/@oxc-parser/binding-android-arm-eabi": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.127.0.tgz", - "integrity": "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.127.0.tgz", - "integrity": "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-darwin-arm64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.127.0.tgz", - "integrity": "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.127.0.tgz", - "integrity": "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.127.0.tgz", - "integrity": "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.127.0.tgz", - "integrity": "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.127.0.tgz", - "integrity": "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.127.0.tgz", - "integrity": "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.127.0.tgz", - "integrity": "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.127.0.tgz", - "integrity": "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.127.0.tgz", - "integrity": "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-riscv64-musl": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.127.0.tgz", - "integrity": "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-s390x-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.127.0.tgz", - "integrity": "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.127.0.tgz", - "integrity": "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-musl": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.127.0.tgz", - "integrity": "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-openharmony-arm64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.127.0.tgz", - "integrity": "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.127.0.tgz", - "integrity": "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-win32-arm64-msvc": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.127.0.tgz", - "integrity": "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-win32-ia32-msvc": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.127.0.tgz", - "integrity": "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-win32-x64-msvc": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.127.0.tgz", - "integrity": "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@oxc-resolver/binding-android-arm-eabi": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.2.tgz", - "integrity": "sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@oxc-resolver/binding-android-arm64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.2.tgz", - "integrity": "sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.2.tgz", - "integrity": "sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.2.tgz", - "integrity": "sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.2.tgz", - "integrity": "sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.2.tgz", - "integrity": "sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.2.tgz", - "integrity": "sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.2.tgz", - "integrity": "sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.2.tgz", - "integrity": "sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.2.tgz", - "integrity": "sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.2.tgz", - "integrity": "sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.2.tgz", - "integrity": "sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.2.tgz", - "integrity": "sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.2.tgz", - "integrity": "sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.2.tgz", - "integrity": "sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-openharmony-arm64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.2.tgz", - "integrity": "sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.2.tgz", - "integrity": "sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.0", - "@emnapi/runtime": "1.11.0", - "@napi-rs/wasm-runtime": "^1.1.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", - "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", - "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.2.tgz", - "integrity": "sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz", - "integrity": "sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", - "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", - "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", - "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", - "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", - "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", - "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", - "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", - "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", - "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", - "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", - "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", - "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", - "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", - "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", - "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/pluginutils": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.5.1.tgz", - "integrity": "sha512-P9ZvnfouRu4MSC+ilERaw6FmZs8zqKhpHxEs4aGDGWQAMzQGXnhvCSvoNwokfNvdt7t3Y5Y4p6gStQgezeIqug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@storybook/addon-docs": { - "version": "10.6.3", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.6.3.tgz", - "integrity": "sha512-365f712au4w/xvFqEKY6TIvTeGGAY7FKRB8zP4CFa3VSv9oyxHaq/ublFg4YDbCeRagppwl1PZ6k4+PoUor3+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mdx-js/react": "^3.0.0", - "@storybook/icons": "^2.0.2", - "@storybook/react-dom-shim": "10.6.3", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "ts-dedent": "^2.0.0", - "unplugin": "^2.3.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.6.3" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@storybook/builder-vite": { - "version": "10.6.3", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.6.3.tgz", - "integrity": "sha512-1+WCZZg/fPxxyHDB5sFKniFt9/4RFUIzoNqMujv+BLRROM+gQeyUHvdvoyZOZ51+P8ee+/PgnNKHjsTupV+nOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^10.6.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@storybook/global": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", - "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@storybook/icons": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.2.0.tgz", - "integrity": "sha512-FpUB/xizIrV1ILhCIYFo+TtQNN9YEoLjfcnAW0JtOXL+BcM2aisSysI0B54JMWDnljriXySBHgN6ps4s9lb75A==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@storybook/react": { - "version": "10.6.3", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.6.3.tgz", - "integrity": "sha512-3tfAkZNdUyx3I4IjM8dgbNWg4k0gj89Hi1MPiawfTybRRWhvnX2YqV9Ut4mrud7YzRv0v+U7+ZYEvj+maUPaow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.6.3", - "react-docgen": "^8.0.2", - "react-docgen-typescript": "^2.2.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.6.3", - "typescript": ">= 4.9.x" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-dom-shim": { - "version": "10.6.3", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.6.3.tgz", - "integrity": "sha512-cJUuPrieiTl06kzlgaU89iG+CtLAgUTt0Xdvm8qFT9yR2qwafSaswhwYWlZCNX4nWuOu7eK7uzWTwzKqyDBOWQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.6.3" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/react-vite": { - "version": "10.6.3", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.6.3.tgz", - "integrity": "sha512-geWwLi9JnaRG2t9bYBfz+94skARzzXNT1Toaf0lk7Oog13rtNK7ay2pWNm9SP6/L2S7mEjoWu9+zHLpwxXvfIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", - "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.6.3", - "@storybook/react": "10.6.3", - "empathic": "^2.0.0", - "magic-string": "^1.1.0", - "react-docgen": "^8.0.2", - "resolve": "^1.22.8", - "tsconfig-paths": "^4.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.6.3", - "typescript": ">= 4.9.x", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", - "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.24.1", - "jiti": "^2.7.0", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.3" - } - }, - "node_modules/@tailwindcss/node/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", - "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-x64": "4.3.3", - "@tailwindcss/oxide-freebsd-x64": "4.3.3", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-x64-musl": "4.3.3", - "@tailwindcss/oxide-wasm32-wasi": "4.3.3", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", - "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", - "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", - "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", - "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", - "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", - "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", - "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", - "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", - "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", - "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", - "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", - "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", - "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.3.3", - "@tailwindcss/oxide": "4.3.3", - "tailwindcss": "4.3.3" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, - "node_modules/@tanstack/react-table": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", - "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", - "license": "MIT", - "dependencies": { - "@tanstack/table-core": "8.21.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.14.22", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.22.tgz", - "integrity": "sha512-zNiB9IF3prySTfAUrj9tPHjD/lonBYkfxrPP4v4ITUx+qwHajJR5cY/xirjvwc+KWkur10GX3TpsbKh7JkD8Lw==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.19.7" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/table-core": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", - "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.19.7", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.19.7.tgz", - "integrity": "sha512-gxy5SvH0mFRJ0YujtrflB/1HAu9Wn1KGvL0bjZfQkYVVMsIpQiwcf1mR06/ViN4NjllUb4HNCjqE9FGwgTvMCQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", - "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.8", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.8.tgz", - "integrity": "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", - "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/doctrine": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", - "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdx": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", - "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.9.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.9.tgz", - "integrity": "sha512-b4e2xxj/yMeT2hNlD6x7zNFiQK2Dlmkb9CQ6v3/1G2RezKN4YS2lcEv8lzGm1mhs4f2lM4yNAMI/zUDBZBBt7w==", - "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, - "node_modules/@types/react": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", - "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", - "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.3.0" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", - "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz", - "integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/type-utils": "8.72.0", - "@typescript-eslint/utils": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.72.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", - "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz", - "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", - "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.72.0", - "@typescript-eslint/types": "^8.72.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", - "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", - "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz", - "integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0", - "@typescript-eslint/utils": "8.72.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz", - "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", - "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.72.0", - "@typescript-eslint/tsconfig-utils": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz", - "integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", - "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.72.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", - "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "oxc-transform-react": "^0.152.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "oxc-transform-react": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@webcontainer/env": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@webcontainer/env/-/env-1.1.1.tgz", - "integrity": "sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", - "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/apache-arrow": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-21.2.0.tgz", - "integrity": "sha512-Hxe6Agq26gQOM954qpzYSllJBPJl+e16U5CkfuMUhLrNba+5nKkttIVlflaovN6oaTratqMGAO8H5u/aNhmHWQ==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "^25.2.0", - "flatbuffers": "^25.1.24", - "json-with-bigint": "^3.5.3", - "tslib": "^2.6.2" - }, - "bin": { - "arrow2csv": "bin/arrow2csv.js" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-types": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.3.tgz", - "integrity": "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.33", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.33.tgz", - "integrity": "sha512-yiVKDX+qCMRKBE084lXvq9EcwFUhKZ2nU8TK1EQQuOgPNR/lmbGxXOeDdlGUqC2Zz11Cs9rk1d/8NfgBmlkt0w==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/browserslist": { - "version": "4.29.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", - "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.11.29", - "caniuse-lite": "^1.0.30001816", - "electron-to-chromium": "^1.5.459", - "node-releases": "^2.0.59", - "update-browserslist-db": "^1.3.4" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bundle-name": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", - "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacheable": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", - "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cacheable/memory": "^2.2.0", - "@cacheable/utils": "^2.5.0", - "hookified": "^1.15.0", - "keyv": "^5.6.0", - "qified": "^0.10.1" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001819", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", - "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/default-browser": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", - "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.464", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", - "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", - "dev": true, - "license": "ISC" - }, - "node_modules/empathic": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.1.0.tgz", - "integrity": "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", - "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz", - "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==", - "dev": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.7.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.3", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "11.1.5 || >11.1.6 <12", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.7.tgz", - "integrity": "sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-plugin-storybook": { - "version": "10.6.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.6.3.tgz", - "integrity": "sha512-AmM0vKC6iaw6geKS3gAwLMnjIsfXxddYcm+zPSRX98TbFm1gAHHeYZtOCm4iEu5fcLIdeUlmDmHihqMVjSw+lw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "^8.60.0", - "@typescript-eslint/utils": "^8.60.0" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", - "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^6.1.23" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "6.1.23", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", - "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cacheable": "^2.5.0", - "flatted": "^3.4.2", - "hookified": "^1.15.0" - } - }, - "node_modules/flatbuffers": { - "version": "25.9.23", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", - "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", - "license": "Apache-2.0" - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", - "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/hashery": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", - "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^1.15.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", - "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-with-bigint": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.12.tgz", - "integrity": "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.4.tgz", - "integrity": "sha512-jQz9sVhrZuphH4NrAIHSgkDJCluQO7Ioi4KGRLa7aTUjAODQ13hENX0YIytqYE/rIkO485gQG2LeBxJaW53kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.6.0" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", - "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.60", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", - "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/oxc-parser": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.127.0.tgz", - "integrity": "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "^0.127.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxc-parser/binding-android-arm-eabi": "0.127.0", - "@oxc-parser/binding-android-arm64": "0.127.0", - "@oxc-parser/binding-darwin-arm64": "0.127.0", - "@oxc-parser/binding-darwin-x64": "0.127.0", - "@oxc-parser/binding-freebsd-x64": "0.127.0", - "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", - "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", - "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", - "@oxc-parser/binding-linux-arm64-musl": "0.127.0", - "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", - "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", - "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", - "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", - "@oxc-parser/binding-linux-x64-gnu": "0.127.0", - "@oxc-parser/binding-linux-x64-musl": "0.127.0", - "@oxc-parser/binding-openharmony-arm64": "0.127.0", - "@oxc-parser/binding-wasm32-wasi": "0.127.0", - "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", - "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", - "@oxc-parser/binding-win32-x64-msvc": "0.127.0" - } - }, - "node_modules/oxc-resolver": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.2.tgz", - "integrity": "sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxc-resolver/binding-android-arm-eabi": "11.21.2", - "@oxc-resolver/binding-android-arm64": "11.21.2", - "@oxc-resolver/binding-darwin-arm64": "11.21.2", - "@oxc-resolver/binding-darwin-x64": "11.21.2", - "@oxc-resolver/binding-freebsd-x64": "11.21.2", - "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.2", - "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.2", - "@oxc-resolver/binding-linux-arm64-gnu": "11.21.2", - "@oxc-resolver/binding-linux-arm64-musl": "11.21.2", - "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.2", - "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.2", - "@oxc-resolver/binding-linux-riscv64-musl": "11.21.2", - "@oxc-resolver/binding-linux-s390x-gnu": "11.21.2", - "@oxc-resolver/binding-linux-x64-gnu": "11.21.2", - "@oxc-resolver/binding-linux-x64-musl": "11.21.2", - "@oxc-resolver/binding-openharmony-arm64": "11.21.2", - "@oxc-resolver/binding-wasm32-wasi": "11.21.2", - "@oxc-resolver/binding-win32-arm64-msvc": "11.21.2", - "@oxc-resolver/binding-win32-x64-msvc": "11.21.2" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", - "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", - "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.20", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", - "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qified": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", - "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^2.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/qified/node_modules/hookified": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", - "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", - "dev": true, - "license": "MIT" - }, - "node_modules/react": { - "version": "19.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", - "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-docgen": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.4.tgz", - "integrity": "sha512-gIYjtVTU4NCWZnb/LXVSWfClZXnjzX3+xxi69Tkfu4YsmYuqiOk8giLkKt/WwxWqh+U66CTh8Kqbha1uPv8/qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.2", - "@types/babel__core": "^7.20.5", - "@types/babel__traverse": "^7.20.7", - "@types/doctrine": "^0.0.9", - "@types/resolve": "^1.20.2", - "doctrine": "^3.0.0", - "resolve": "^1.22.1", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": "^20.9.0 || >=22" - } - }, - "node_modules/react-docgen-typescript": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", - "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typescript": ">= 4.3.x" - } - }, - "node_modules/react-dom": { - "version": "19.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", - "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.28.0" - }, - "peerDependencies": { - "react": "^19.3.1" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/recast": { - "version": "0.23.21", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", - "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/redent/node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve": { - "version": "1.22.13", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", - "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.17.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rolldown": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", - "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.155.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.15", - "@rolldown/binding-android-arm64": "1.2.15", - "@rolldown/binding-darwin-arm64": "1.2.15", - "@rolldown/binding-darwin-x64": "1.2.15", - "@rolldown/binding-freebsd-x64": "1.2.15", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", - "@rolldown/binding-linux-arm64-gnu": "1.2.15", - "@rolldown/binding-linux-arm64-musl": "1.2.15", - "@rolldown/binding-linux-ppc64-gnu": "1.2.15", - "@rolldown/binding-linux-s390x-gnu": "1.2.15", - "@rolldown/binding-linux-x64-gnu": "1.2.15", - "@rolldown/binding-linux-x64-musl": "1.2.15", - "@rolldown/binding-openharmony-arm64": "1.2.15", - "@rolldown/binding-win32-arm64-msvc": "1.2.15", - "@rolldown/binding-win32-x64-msvc": "1.2.15" - } - }, - "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.155.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", - "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/scheduler": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", - "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", - "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/storybook": { - "version": "10.6.3", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.6.3.tgz", - "integrity": "sha512-lNBvcZFG44C98KvQHYzfq1O46m+sl94zY1+na5/9E3ihtNMbvpOgYyMFMIcWYUvOMo3OM4q9jzvB73KU20bU8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "@storybook/icons": "^2.0.2", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "6.9.1", - "@testing-library/user-event": "^14.6.3", - "@vitest/expect": "3.2.4", - "@vitest/spy": "3.2.4", - "@webcontainer/env": "^1.1.1", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", - "jsonc-parser": "^3.3.1", - "open": "^10.2.0", - "oxc-parser": "^0.127.0", - "oxc-resolver": "11.21.2", - "recast": "^0.23.5", - "semver": "^7.7.3", - "use-sync-external-store": "^1.5.0", - "ws": "^8.21.1" - }, - "bin": { - "storybook": "dist/bin/dispatcher.js" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "prettier": "^2 || ^3", - "vite-plus": "^0.1.15 || ^0.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "prettier": { - "optional": true - }, - "vite-plus": { - "optional": true - } - } - }, - "node_modules/storybook/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-indent": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", - "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", - "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", - "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-dedent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", - "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.10" - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz", - "integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.72.0", - "@typescript-eslint/parser": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0", - "@typescript-eslint/utils": "8.72.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "license": "MIT" - }, - "node_modules/unplugin": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", - "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", - "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", - "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/vite": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", - "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.7", - "postcss": "^8.5.30", - "rolldown": "~1.2.14", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1 || ^0.8.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/vite/node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ws": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", - "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.6.7", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", - "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} diff --git a/vortex-web/package.json b/vortex-web/package.json index de101be00fd..8f23d3c87bc 100644 --- a/vortex-web/package.json +++ b/vortex-web/package.json @@ -3,49 +3,44 @@ "private": true, "version": "0.1.0", "type": "module", + "packageManager": "bun@1.4.3", + "engines": { + "node": ">=24" + }, "scripts": { "wasm": "wasm-pack build crate --dev --target web --out-dir ../src/wasm/pkg", "wasm:release": "wasm-pack build crate --profile wasm_release --target web --out-dir ../src/wasm/pkg", - "dev": "npm run wasm && vite", - "build": "npm run wasm:release && tsc -b && vite build", - "check": "npm run wasm && npm run lint && npm run typecheck", + "dev": "bun run wasm && vite", + "build": "bun run wasm:release && tsc -b && vite build", + "check": "bun run wasm && biome check && bun run typecheck", "preview": "vite preview", - "format": "prettier --write 'src/**/*.{ts,tsx,css}' '.storybook/**/*.ts'", - "format:check": "prettier --check 'src/**/*.{ts,tsx,css}' '.storybook/**/*.ts'", - "lint": "eslint .", - "lint:fix": "eslint . --fix", + "format": "biome format --write", + "format:check": "biome format", + "lint": "biome lint", + "lint:fix": "biome lint --write", "typecheck": "tsc -b --noEmit", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" }, "dependencies": { - "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.23", - "apache-arrow": "^21.1.0", + "@tanstack/react-table": "^9.3.1", + "@tanstack/react-virtual": "^3.14.23", + "apache-arrow": "^21.2.0", "d3-hierarchy": "^3.1.2", - "react": "^19.1.0", - "react-dom": "^19.1.0" + "react": "^19.3.1", + "react-dom": "^19.3.1" }, "devDependencies": { - "@eslint/js": "^10.0.1", - "@storybook/addon-docs": "^10.3.5", - "@storybook/react-vite": "^10.3.5", - "@tailwindcss/vite": "^4.2.4", + "@biomejs/biome": "^2.6.2", + "@storybook/addon-docs": "^10.6.5", + "@storybook/react-vite": "^10.6.5", "@types/d3-hierarchy": "^3.1.7", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^10.2.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "eslint-plugin-storybook": "^10.3.5", - "globals": "^17.5.0", - "prettier": "^3.8.3", - "storybook": "^10.3.5", - "tailwindcss": "^4.2.4", - "typescript": "~6.0.0", - "typescript-eslint": "^8.59.0", - "vite": "^8.0.10" - } + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "storybook": "^10.6.5", + "typescript": "^7.0.2", + "vite": "^8.3.5" + }, + "trustedDependencies": [] } diff --git a/vortex-web/public/theme.js b/vortex-web/public/theme.js new file mode 100644 index 00000000000..f089da274e1 --- /dev/null +++ b/vortex-web/public/theme.js @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +// Sets the saved theme before the first paint, so a light theme never flashes dark. A file, not an +// inline script, because the Content-Security-Policy allows scripts from this origin only. The key +// is the system ThemeSwitch's, 'vx-theme'; a choice saved under the Explorer's old key moves to it. +try { + const old = localStorage.getItem('vortex-theme'); + if (old !== null) { + if (localStorage.getItem('vx-theme') === null) localStorage.setItem('vx-theme', old); + localStorage.removeItem('vortex-theme'); + } + const choice = localStorage.getItem('vx-theme'); + if (choice === 'light' || choice === 'dark' || choice === 'system') { + document.documentElement.dataset.theme = choice; + } +} catch { + // Storage can be blocked; the page then stays on the default, dark. +} diff --git a/vortex-web/public/vortex_wordmark.svg b/vortex-web/public/vortex_wordmark.svg new file mode 100644 index 00000000000..69b39d4e973 --- /dev/null +++ b/vortex-web/public/vortex_wordmark.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vortex-web/public/vortex_wordmark_dark_theme.svg b/vortex-web/public/vortex_wordmark_dark_theme.svg new file mode 100644 index 00000000000..1af1b2584e0 --- /dev/null +++ b/vortex-web/public/vortex_wordmark_dark_theme.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vortex-web/src/App.tsx b/vortex-web/src/App.tsx index 8fd119fa8e0..3a816bfbcdb 100644 --- a/vortex-web/src/App.tsx +++ b/vortex-web/src/App.tsx @@ -86,7 +86,7 @@ function App() { setFileState((prev) => { if (!prev) return prev; const node = findNodeById(prev.layoutTree, nodeId); - if (!node || node.encoding !== 'vortex.flat') return prev; + if (node?.encoding !== 'vortex.flat') return prev; if (node.children.some((c) => c.isArrayNode)) return prev; const arrayChildren = arrayTreeToLayoutChildren(arrayTree, node); @@ -169,10 +169,12 @@ function App() { } return ( - - + + + {/* The whole window takes a dropped file, so a new file can replace the open one. */} + {/* biome-ignore lint/a11y/noStaticElementInteractions: drag and drop has no keyboard form; Close and the open screen's button do. */}

{isDragging && ( -
-

Drop to open file

+
+

Drop to open the file

)}
diff --git a/vortex-web/src/components/DataTable.tsx b/vortex-web/src/components/DataTable.tsx index e91970081d1..c34d18ead0e 100644 --- a/vortex-web/src/components/DataTable.tsx +++ b/vortex-web/src/components/DataTable.tsx @@ -3,14 +3,20 @@ import { useMemo, useRef, useState } from 'react'; import { - useReactTable, - getCoreRowModel, - getSortedRowModel, + columnSizingFeature, + createSortedRowModel, flexRender, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_datetime, + sortFn_text, + tableFeatures, + useTable, type ColumnDef, type SortingState, } from '@tanstack/react-table'; import { useVirtualizer } from '@tanstack/react-virtual'; +import { Facts } from './detail/Facts'; // --- Column statistics --- @@ -69,7 +75,7 @@ function computeStats(values: unknown[]): ColumnStats { const range = max - min; for (const n of nums) { const idx = Math.min(bins - 1, Math.floor(((n - min) / range) * bins)); - histogram[idx]++; + histogram[idx] = (histogram[idx] ?? 0) + 1; } } else { histogram[0] = nums.length; @@ -88,50 +94,25 @@ function computeStats(values: unknown[]): ColumnStats { // --- Sparkline --- +const BAR = 2; +const GAP = 0.5; + +/** A small histogram of a numeric column. A column with one value draws a flat line. */ function SparkHistogram({ histogram, height = 12 }: { histogram: number[]; height?: number }) { const max = Math.max(...histogram); if (max === 0) return null; - - // Check if all values fell in a single bin (constant column). - const nonZero = histogram.filter((v) => v > 0).length; - if (nonZero <= 1) { - // Render a flat line to indicate constant/single-value. - const barW = 2; - const gap = 0.5; - const w = histogram.length * (barW + gap) - gap; - return ( - - - - ); - } - - const barW = 2; - const gap = 0.5; - const w = histogram.length * (barW + gap) - gap; + const w = histogram.length * (BAR + GAP) - GAP; + const flat = histogram.filter((v) => v > 0).length <= 1; return ( - - {histogram.map((v, i) => { - const barH = Math.max(0.5, (v / max) * height); - return ( - - ); - })} + ); } @@ -139,131 +120,70 @@ function SparkHistogram({ histogram, height = 12 }: { histogram: number[]; heigh // --- Header summary (inline, compact) --- function HeaderSummary({ stats }: { stats: ColumnStats }) { + const note = (text: string, title?: string) => ( + + {text} + + ); if (stats.kind === 'numeric') { - const isConst = stats.min === stats.max && stats.nullCount === 0; - if (isConst) { - return ( - - const - - ); - } - if (stats.histogram) { - return ; - } + if (stats.min === stats.max && stats.nullCount === 0) + return note('const', `Every value is ${stats.min}`); + if (stats.histogram) return ; } if (stats.kind === 'boolean') { - const total = stats.trueCount! + stats.falseCount!; - if (total === 0) return null; - const allTrue = stats.falseCount === 0 && stats.nullCount === 0; - const allFalse = stats.trueCount === 0 && stats.nullCount === 0; - if (allTrue || allFalse) - return const; - const pct = Math.round((stats.trueCount! / total) * 100); + const t = stats.trueCount ?? 0; + const f = stats.falseCount ?? 0; + if (t + f === 0) return null; + if ((t === 0 || f === 0) && stats.nullCount === 0) return note('const'); + const pct = Math.round((t / (t + f)) * 100); return ( -
-
-
-
-
- {pct}% -
+ + ); } if (stats.kind === 'string' && stats.cardinality != null) { - if (stats.cardinality === 1 && stats.nullCount === 0) { - return const; - } - return ( - - {stats.cardinality}v - - ); + if (stats.cardinality === 1 && stats.nullCount === 0) return note('const'); + return note(`${stats.cardinality.toLocaleString('en-US')} distinct`); } return null; } -// --- Header tooltip (shown on hover) --- +// --- Header tooltip (shown on hover or focus) --- function HeaderTooltip({ stats, approximate }: { stats: ColumnStats; approximate: boolean }) { const p = approximate ? '~' : ''; - const fmt = (n: number) => { - const s = - Math.abs(n) >= 1e6 || (Math.abs(n) < 0.01 && n !== 0) - ? n.toExponential(2) - : n.toLocaleString(undefined, { maximumFractionDigits: 2 }); - return p + s; - }; - - return ( -
-
- {stats.count.toLocaleString()} rows{approximate ? ' (sampled)' : ''} -
- {stats.nullCount > 0 && ( -
{stats.nullCount.toLocaleString()} nulls
- )} - {stats.kind === 'numeric' && ( - <> -
min: {fmt(stats.min!)}
-
max: {fmt(stats.max!)}
-
mean: {fmt(stats.mean!)}
- {stats.histogram && ( -
- -
- )} - - )} - {stats.kind === 'boolean' && ( - <> -
true: {stats.trueCount!.toLocaleString()}
-
false: {stats.falseCount!.toLocaleString()}
- - )} - {stats.kind === 'string' &&
{stats.cardinality!.toLocaleString()} distinct values
} -
- ); -} - -// --- Hoverable header with tooltip --- - -function ColumnHeader({ - name, - stats, - approximate, -}: { - name: string; - stats: ColumnStats; - approximate: boolean; -}) { - const [showTip, setShowTip] = useState(false); - const timeoutRef = useRef>(undefined); - - const onEnter = () => { - clearTimeout(timeoutRef.current); - timeoutRef.current = setTimeout(() => setShowTip(true), 400); - }; - const onLeave = () => { - clearTimeout(timeoutRef.current); - setShowTip(false); - }; - + const fmt = (n: number) => + p + + (Math.abs(n) >= 1e6 || (Math.abs(n) < 0.01 && n !== 0) + ? n.toExponential(2) + : n.toLocaleString('en-US', { maximumFractionDigits: 2 })); + const items: [string, string][] = [ + ['Rows', `${stats.count.toLocaleString('en-US')}${approximate ? ' (sample)' : ''}`], + ]; + if (stats.nullCount > 0) items.push(['Nulls', stats.nullCount.toLocaleString('en-US')]); + if (stats.kind === 'numeric') { + items.push( + ['Min', fmt(stats.min ?? 0)], + ['Max', fmt(stats.max ?? 0)], + ['Mean', fmt(stats.mean ?? 0)], + ); + } + if (stats.kind === 'boolean') { + items.push(['True', (stats.trueCount ?? 0).toLocaleString('en-US')]); + items.push(['False', (stats.falseCount ?? 0).toLocaleString('en-US')]); + } + if (stats.kind === 'string') + items.push(['Distinct', (stats.cardinality ?? 0).toLocaleString('en-US')]); return ( -
-
- {name} - -
- {showTip && ( -
- +
+ + {stats.kind === 'numeric' && stats.histogram && ( +
+
)}
@@ -276,88 +196,105 @@ function formatCell(value: unknown): string { if (value == null) return ''; if (typeof value === 'bigint') return value.toString(); if (typeof value === 'number') { - if (Number.isInteger(value)) return value.toLocaleString(); - return value.toLocaleString(undefined, { maximumFractionDigits: 4 }); + if (Number.isInteger(value)) return value.toLocaleString('en-US'); + return value.toLocaleString('en-US', { maximumFractionDigits: 4 }); } return String(value); } // --- Main component --- +// Sorting by column header, and column widths. 'auto' sorting picks one of the +// registered sort functions from the column's values, and falls back to basic. +const features = tableFeatures({ + rowSortingFeature, + columnSizingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { alphanumeric: sortFn_alphanumeric, datetime: sortFn_datetime, text: sortFn_text }, +}); + +type Row = Record; + export type CellRenderer = (value: unknown, row: Record) => React.ReactNode; export interface DataTableProps { + /** The table's accessible name. */ + label: string; columns: string[]; rows: Record[]; - rowHeight?: number; onRowClick?: (rowIndex: number) => void; onRowHover?: (rowIndex: number | null) => void; /** Custom cell renderers keyed by column name. */ cellRenderers?: Record; + /** Columns to set right-aligned, as numbers. The preview finds them from the values. */ + numeric?: ReadonlySet; /** If true, stats are approximate (data was truncated by a row limit). */ approximate?: boolean; } +/** The row height the system sets for a data table (--app-row). */ +const ROW_HEIGHT = 36; + +/** + * A data table on the system's vx-table: sticky heads that sort, virtualised rows, and a short + * summary of each column in its head, with the column's numbers on hover or focus. + */ export function DataTable({ + label, columns, rows, - rowHeight = 24, onRowClick, onRowHover, cellRenderers, + numeric, approximate = false, }: DataTableProps) { const [sorting, setSorting] = useState([]); + const [tipColumn, setTipColumn] = useState(null); + const tipTimer = useRef>(undefined); const parentRef = useRef(null); const columnStats = useMemo(() => { const stats: Record = {}; - for (const col of columns) { - stats[col] = computeStats(rows.map((r) => r[col])); - } + for (const col of columns) stats[col] = computeStats(rows.map((r) => r[col])); return stats; }, [columns, rows]); - const columnDefs = useMemo>[]>( + const isNum = (col: string) => numeric?.has(col) ?? columnStats[col]?.kind === 'numeric'; + + const columnDefs = useMemo[]>( () => [ { id: '__row_num', header: '#', - size: 50, + size: 56, enableSorting: false, - cell: (info) => ( - {info.row.index} - ), + cell: (info) => {info.row.index}, }, - ...columns.map((col): ColumnDef> => ({ - accessorKey: col, - header: () => ( - - ), - cell: (info) => { - const renderer = cellRenderers?.[col]; - if (renderer) { - return renderer(info.getValue(), info.row.original); - } - const val = info.getValue(); - if (val == null) { - return null; - } - return formatCell(val); - }, - sortingFn: 'auto', - })), + ...columns.map( + (col): ColumnDef => ({ + accessorKey: col, + header: col, + cell: (info) => { + const renderer = cellRenderers?.[col]; + if (renderer) return renderer(info.getValue(), info.row.original); + const val = info.getValue(); + if (val == null) return null; + return formatCell(val); + }, + sortFn: 'auto', + }), + ), ], - [approximate, cellRenderers, columns, columnStats], + [cellRenderers, columns], ); - const table = useReactTable({ + const table = useTable({ + features, data: rows, columns: columnDefs, state: { sorting }, onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), }); const { rows: tableRows } = table.getRowModel(); @@ -365,64 +302,102 @@ export function DataTable({ const virtualizer = useVirtualizer({ count: tableRows.length, getScrollElement: () => parentRef.current, - estimateSize: () => rowHeight, + estimateSize: () => ROW_HEIGHT, overscan: 20, }); + const items = virtualizer.getVirtualItems(); + + const showTip = (col: string | null, delay = 400) => { + clearTimeout(tipTimer.current); + if (col === null) setTipColumn(null); + else tipTimer.current = setTimeout(() => setTipColumn(col), delay); + }; return ( -
- - +
+
+ {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + {headerGroup.headers.map((header) => { + const col = header.column.id; + const sorted = header.column.getIsSorted(); + const stats = columnStats[col]; + const canSort = header.column.getCanSort(); + const title = flexRender(header.column.columnDef.header, header.getContext()); + return ( + - ))} + + ); + })} ))} - {virtualizer.getVirtualItems().length > 0 && ( - - + )} - {virtualizer.getVirtualItems().map((virtualRow) => { + {items.map((virtualRow) => { const row = tableRows[virtualRow.index]; + if (!row) return null; + const open = onRowClick ? () => onRowClick(virtualRow.index) : undefined; return ( onRowClick?.(virtualRow.index)} - onMouseEnter={() => onRowHover?.(virtualRow.index)} - onMouseLeave={() => onRowHover?.(null)} + aria-rowindex={virtualRow.index + 2} + className={open ? 'is-link' : undefined} + tabIndex={open ? 0 : undefined} + onClick={open} + onKeyDown={ + open + ? (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + open(); + } + } + : undefined + } + onPointerEnter={onRowHover ? () => onRowHover(virtualRow.index) : undefined} + onPointerLeave={onRowHover ? () => onRowHover(null) : undefined} > - {row.getVisibleCells().map((cell) => ( + {row.getAllCells().map((cell) => ( @@ -430,15 +405,11 @@ export function DataTable({ ); })} - {virtualizer.getVirtualItems().length > 0 && ( - + {items.length > 0 && ( + )} diff --git a/vortex-web/src/components/Icon.tsx b/vortex-web/src/components/Icon.tsx new file mode 100644 index 00000000000..60596d9fe40 --- /dev/null +++ b/vortex-web/src/components/Icon.tsx @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +// The design system's icons (Material Symbols, 24px grid, viewBox 0 -960 960 960). Each draws in +// currentColor, or in the fill its class sets. +const PATHS = { + chevron_right: 'M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z', + close: + 'm256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z', + contrast: + 'M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm40-83q119-15 199.5-104.5T800-480q0-123-80.5-212.5T520-797v634Z', + dark_mode: + 'M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q14 0 27.5 1t26.5 3q-41 29-65.5 75.5T444-660q0 90 63 153t153 63q55 0 101-24.5t75-65.5q2 13 3 26.5t1 27.5q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z', + light_mode: + 'M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM200-440H40v-80h160v80Zm720 0H760v-80h160v80ZM440-760v-160h80v160h-80Zm0 720v-160h80v160h-80ZM256-650l-101-97 57-59 96 100-52 56Zm492 496-97-101 53-55 101 97-57 59Zm-98-550 97-101 59 57-100 96-56-52ZM154-212l101-97 55 53-97 101-59-57Zm326-268Z', + upload_file: + 'M440-200h80v-167l64 64 56-57-160-160-160 160 57 56 63-63v167ZM160-80v-800h400l240 240v560H160Zm360-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z', + menu: 'M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z', +} as const; + +export type IconName = keyof typeof PATHS; + +export function Icon({ name, className }: { name: IconName; className?: string }) { + return ( + + ); +} diff --git a/vortex-web/src/components/Switch.tsx b/vortex-web/src/components/Switch.tsx new file mode 100644 index 00000000000..9384b518751 --- /dev/null +++ b/vortex-web/src/components/Switch.tsx @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +/** The design system's text switch (vx-switch): two or more choices, the current one pressed. */ +export function Switch({ + label, + value, + options, + onChange, +}: { + label: string; + value: T; + options: readonly { value: T; label: string }[]; + onChange: (value: T) => void; +}) { + return ( +
+ {options.map((o) => ( + + ))} +
+ ); +} diff --git a/vortex-web/src/components/ThemePicker.tsx b/vortex-web/src/components/ThemePicker.tsx index 3092ff6f20a..5d726d1bde3 100644 --- a/vortex-web/src/components/ThemePicker.tsx +++ b/vortex-web/src/components/ThemePicker.tsx @@ -1,95 +1,32 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -import type React from 'react'; import { useTheme, type ThemeChoice } from '../contexts/ThemeContextCore'; +import { Icon, type IconName } from './Icon'; -const cycle: ThemeChoice[] = ['dark', 'light', 'system']; - -const labels: Record = { - light: 'Light mode', - dark: 'Dark mode', - system: 'System theme', -}; - -function SunIcon() { - return ( - - - - - ); -} - -function MoonIcon() { - return ( - - - - ); -} - -function MonitorIcon() { - return ( - - - - - - ); -} - -const icons: Record React.JSX.Element> = { - light: SunIcon, - dark: MoonIcon, - system: MonitorIcon, -}; +const CHOICES: { choice: ThemeChoice; label: string; icon: IconName }[] = [ + { choice: 'light', label: 'Light', icon: 'light_mode' }, + { choice: 'dark', label: 'Dark', icon: 'dark_mode' }, + { choice: 'system', label: 'System', icon: 'contrast' }, +]; +/** The design system's theme switch: three buttons, the current one pressed. */ export function ThemePicker() { const { theme, setTheme } = useTheme(); - - const next = () => { - const idx = cycle.indexOf(theme); - setTheme(cycle[(idx + 1) % cycle.length]); - }; - - const Icon = icons[theme]; - return ( - +
+ {CHOICES.map(({ choice, label, icon }) => ( + + ))} +
); } diff --git a/vortex-web/src/components/detail/ArraySummaryPane.tsx b/vortex-web/src/components/detail/ArraySummaryPane.tsx index 06a2311118a..68ceffb1bd8 100644 --- a/vortex-web/src/components/detail/ArraySummaryPane.tsx +++ b/vortex-web/src/components/detail/ArraySummaryPane.tsx @@ -3,38 +3,32 @@ import type { LayoutTreeNode } from '../swimlane/types'; import { formatBytes, getNodeDisplayName, shortEncoding } from '../swimlane/utils'; +import { Facts } from './Facts'; interface ArraySummaryPaneProps { node: LayoutTreeNode; } export function ArraySummaryPane({ node }: ArraySummaryPaneProps) { - const name = getNodeDisplayName(node); - const totalBufferBytes = (node.bufferLengths ?? []).reduce((s, b) => s + b, 0); + const buffers = node.bufferLengths ?? []; + const totalBufferBytes = buffers.reduce((s, b) => s + b, 0); return ( -
-

{name}

-
- Encoding - - {shortEncoding(node.encoding)} - - Metadata - - {formatBytes(node.metadataBytes)} - - Buffers - - {(node.bufferLengths ?? []).length} - - Buffer data - - {formatBytes(totalBufferBytes)} - - Children - {node.children.length} + <> +
+

{getNodeDisplayName(node)}

-
+
+ +
+ ); } diff --git a/vortex-web/src/components/detail/BuffersPane.tsx b/vortex-web/src/components/detail/BuffersPane.tsx index ae557abb780..cf16cb21e7f 100644 --- a/vortex-web/src/components/detail/BuffersPane.tsx +++ b/vortex-web/src/components/detail/BuffersPane.tsx @@ -51,38 +51,37 @@ export function BuffersPane({ node }: BuffersPaneProps) { }, [node.id, bufferLengths.length, loadBuffer]); if (bufferLengths.length === 0) { - return
No buffers for this node.
; + return

This node has no buffers.

; } return ( -
- {/* Buffer list — left side */} -
+
+
+ Buffers {bufferLengths.map((len, i) => ( ))} -
- - {/* Hex viewer — right side */} -
- {selectedBuffer === null && ( -
Select a buffer to view its contents.
+ + +
+ {selectedBuffer === null &&

Select a buffer to see its bytes.

} + {loading &&

Reading the buffer…

} + {error && ( +
+

The buffer did not load. Select it again.

+

+ Reason · {error} +

+
)} - {loading &&
Loading…
} - {error &&
Error: {error}
} {bufferData && }
@@ -92,82 +91,59 @@ export function BuffersPane({ node }: BuffersPaneProps) { const BYTES_PER_ROW = 16; const MAX_ROWS = 1024; -interface HexRow { - offset: number; - bytes: { value: number; absIndex: number }[]; -} +const hex = (n: number, width: number) => n.toString(16).padStart(width, '0'); +const printable = (b: number) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : null); +/** + * Offset, hex, and text columns, 16 bytes a row. Hover marks a byte in both columns; the pointer + * is read from the row, so a byte needs no handlers of its own. + */ function HexView({ data }: { data: Uint8Array }) { const [hovered, setHovered] = useState(null); - - const { rows, truncated } = useMemo(() => { - const result: HexRow[] = []; - for (let i = 0; i < data.length; i += BYTES_PER_ROW) { - const bytes: HexRow['bytes'] = []; - const end = Math.min(i + BYTES_PER_ROW, data.length); - for (let j = i; j < end; j++) { - bytes.push({ value: data[j], absIndex: j }); - } - result.push({ offset: i, bytes }); + const rows = Math.min(Math.ceil(data.length / BYTES_PER_ROW), MAX_ROWS); + const truncated = Math.ceil(data.length / BYTES_PER_ROW) > MAX_ROWS; + + const onPointer = (e: React.PointerEvent) => { + const at = (e.target as HTMLElement).dataset.at; + setHovered(at === undefined ? null : Number(at)); + }; + + const lines = useMemo(() => { + const out: { offset: number; bytes: number[] }[] = []; + for (let r = 0; r < rows; r++) { + const offset = r * BYTES_PER_ROW; + out.push({ offset, bytes: Array.from(data.subarray(offset, offset + BYTES_PER_ROW)) }); } - const isTruncated = result.length > MAX_ROWS; - return { rows: isTruncated ? result.slice(0, MAX_ROWS) : result, truncated: isTruncated }; - }, [data]); - - const hlClass = 'bg-vortex-light-blue/20 rounded-sm'; + return out; + }, [data, rows]); return ( -
- {rows.map((row) => ( -
- {/* Offset */} - - {row.offset.toString(16).padStart(8, '0')} - - - {/* Hex bytes */} - - {Array.from({ length: BYTES_PER_ROW }, (_, j) => { - const b = row.bytes[j]; - if (!b) - return ( - - {' '} - - ); - const isHl = hovered === b.absIndex; - const gap = j === 8 ? ' ' : ''; - return ( - - {gap} - setHovered(b.absIndex)} - onMouseLeave={() => setHovered(null)} - > - {b.value.toString(16).padStart(2, '0')} - - - ); - })} + // Hover only marks a byte; nothing acts on it. +
setHovered(null)}> + {lines.map(({ offset, bytes }) => ( +
+ {hex(offset, 8)} + + {bytes.map((b, j) => ( + + {hex(b, 2)} + + ))} - - {/* ASCII */} - - {Array.from({ length: BYTES_PER_ROW }, (_, j) => { - const b = row.bytes[j]; - if (!b) return ; - const isHl = hovered === b.absIndex; - const ch = b.value >= 0x20 && b.value < 0x7f ? String.fromCharCode(b.value) : '.'; - const color = ch === '.' ? 'text-vortex-grey-dark' : 'text-vortex-light-blue'; + + {bytes.map((b, j) => { + const ch = printable(b); return ( setHovered(b.absIndex)} - onMouseLeave={() => setHovered(null)} + data-at={offset + j} + className={`${ch === null ? 'is-dot' : ''}${hovered === offset + j ? ' is-hovered' : ''}`} > - {ch} + {ch ?? '.'} ); })} @@ -175,10 +151,10 @@ function HexView({ data }: { data: Uint8Array }) {
))} {truncated && ( -
- … {Math.ceil(data.length / BYTES_PER_ROW) - MAX_ROWS} more rows ( - {formatBytes(data.length)} total) -
+

+ {(Math.ceil(data.length / BYTES_PER_ROW) - MAX_ROWS).toLocaleString('en-US')} more rows + not shown ({formatBytes(data.length)} in all). +

)}
); diff --git a/vortex-web/src/components/detail/DetailPanel.stories.tsx b/vortex-web/src/components/detail/DetailPanel.stories.tsx index 053eac2d1d8..0c41d53601d 100644 --- a/vortex-web/src/components/detail/DetailPanel.stories.tsx +++ b/vortex-web/src/components/detail/DetailPanel.stories.tsx @@ -29,9 +29,9 @@ const meta: Meta = { decorators: [ withMockFileContext(mockFileState), withMockSelection(layout), - // DetailPanel fills its parent's height; give it a bounded flex container. + // DetailPanel fills its parent's height; give it a bounded box. (Story) => ( -
+
), diff --git a/vortex-web/src/components/detail/DetailPanel.tsx b/vortex-web/src/components/detail/DetailPanel.tsx index b4cd1ec36e7..74e13fc6790 100644 --- a/vortex-web/src/components/detail/DetailPanel.tsx +++ b/vortex-web/src/components/detail/DetailPanel.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -import { useMemo, useState } from 'react'; +import { useId, useMemo, useState, type KeyboardEvent } from 'react'; import { useVortexFile } from '../../contexts/VortexFileContextCore'; import { useSelection } from '../../contexts/SelectionContextCore'; import { @@ -9,9 +9,8 @@ import { findPathToNode, getDtypeCategory, shortEncoding, - DTYPE_COLORS, + dtypeClass, } from '../swimlane/utils'; -import { SummarySidebar } from './SummarySidebar'; import { EncodingPane } from './EncodingPane'; import { SegmentsPane } from './SegmentsPane'; import { BlockTreemap } from '../explorer/BlockTreemap'; @@ -68,116 +67,126 @@ export function DetailPanel() { // Prefer the active tab; otherwise fall back to the first available tab. const currentTab = tabs.find((t) => t.id === activeTab)?.id ?? tabs[0]?.id; + const ids = useId(); + const tabId = (id: TabId) => `${ids}-tab-${id}`; + const panelId = `${ids}-panel`; + + // Left and Right move between the tabs, Home and End to the ends: the WAI-ARIA tabs pattern. + const onTabKey = (e: KeyboardEvent) => { + const at = tabs.findIndex((t) => t.id === currentTab); + const next = { + ArrowRight: at + 1, + ArrowLeft: at - 1, + Home: 0, + End: tabs.length - 1, + }[e.key]; + if (next === undefined || tabs.length === 0) return; + e.preventDefault(); + const tab = tabs[(next + tabs.length) % tabs.length]!; + setActiveTab(tab.id); + document.getElementById(tabId(tab.id))?.focus(); + }; + + const tip = breadcrumb[breadcrumb.length - 1]; + return ( -
- {/* Breadcrumb + tab bar */} -
- {/* Tabs */} -
- {tabs.map((tab) => ( - - ))} -
- - {/* Breadcrumb — right of tabs */} - {breadcrumb.length > 1 && ( -
- {breadcrumb.map((node, i) => { - const isLast = i === breadcrumb.length - 1; - const isShared = isHoverBreadcrumb && selectedIdSet.has(node.id); - const dimClass = isHoverBreadcrumb && !isShared ? 'opacity-50' : ''; - const prevNode = i > 0 ? breadcrumb[i - 1] : null; - const isArrayBoundary = node.isArrayNode && prevNode && !prevNode.isArrayNode; - return ( - - {isArrayBoundary && ( - - › - - )} - {i > 0 && !isArrayBoundary && /} - {isLast && !isHoverBreadcrumb ? ( - - {getNodeDisplayName(node)} - - ) : ( - - )} - - ); - })} - {(() => { - const tip = breadcrumb[breadcrumb.length - 1]; - if (!tip) return null; - const cat = getDtypeCategory(tip.dtype); - return ( - - {cat} - - ); - })()} +
+
+ {tabs.length > 0 && ( +
+ {tabs.map((tab) => ( + + ))}
)} + + {breadcrumb.length > 1 && ( + + )}
- {/* Main content: tab content (left) + summary sidebar (right) */} -
-
- {currentTab === 'encoding' && selection.selectedNode && ( -
- -
- )} - {currentTab === 'segments' && selection.selectedNode && ( - - )} - {currentTab === 'treemap' && selection.selectedNode && ( - - )} - {currentTab === 'buffers' && selection.selectedNode && ( - - )} - {!selection.selectedNode && ( -
- Select a node to view details. -
- )} -
- - {/* Summary sidebar — always visible */} - +
+ {currentTab === 'encoding' && selection.selectedNode && ( + + )} + {currentTab === 'segments' && selection.selectedNode && ( + + )} + {currentTab === 'treemap' && selection.selectedNode && ( + + )} + {currentTab === 'buffers' && selection.selectedNode && ( + + )} + {!selection.selectedNode && ( +

+ Select a column or a layout in the tree to see its treemap, segments, and encoding. +

+ )}
-
+
); } diff --git a/vortex-web/src/components/detail/EncodingPane.tsx b/vortex-web/src/components/detail/EncodingPane.tsx index f496fc8810a..ee0e79c5b0d 100644 --- a/vortex-web/src/components/detail/EncodingPane.tsx +++ b/vortex-web/src/components/detail/EncodingPane.tsx @@ -52,28 +52,25 @@ export function EncodingPane({ node }: EncodingPaneProps) { }, [node.id, node.arrayEncodingTree, node.segmentIds, fetchEncodingTree]); if (loading) { - return
Loading encoding tree…
; + return

Reading the encoding tree…

; } if (error) { - return
Error: {error}
; - } - - if (!tree) { return ( -
- No array encoding tree available for this node. +
+

The encoding tree did not load. Select the node again.

+

+ Reason · {error} +

); } - return ( -
-
-        {renderArrayTree(tree, 0)}
-      
-
- ); + if (!tree) { + return

This node has no array encoding tree.

; + } + + return
{renderArrayTree(tree, 0)}
; } function renderArrayTree(node: ArrayEncodingNode, indent: number): string { diff --git a/vortex-web/src/components/detail/Facts.tsx b/vortex-web/src/components/detail/Facts.tsx new file mode 100644 index 00000000000..c9e1e18b27b --- /dev/null +++ b/vortex-web/src/components/detail/Facts.tsx @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +import type { ReactNode } from 'react'; + +/** The design system's key and value list (vx-facts): each key a label, each value in ink. */ +export function Facts({ + items, + className, +}: { + items: [key: string, value: ReactNode, title?: string][]; + className?: string; +}) { + return ( +
+ {items.map(([key, value, title]) => ( +
+
{key}
+
{value}
+
+ ))} +
+ ); +} diff --git a/vortex-web/src/components/detail/SegmentsPane.tsx b/vortex-web/src/components/detail/SegmentsPane.tsx index 9cb3739d507..e765522d79c 100644 --- a/vortex-web/src/components/detail/SegmentsPane.tsx +++ b/vortex-web/src/components/detail/SegmentsPane.tsx @@ -8,6 +8,8 @@ import { useVortexFile } from '../../contexts/VortexFileContextCore'; import { useSelection } from '../../contexts/SelectionContextCore'; import { DataTable, type CellRenderer } from '../DataTable'; +const NUMERIC = new Set(['index', 'byte_offset', 'byte_length', 'alignment']); + interface SegmentsPaneProps { node: LayoutTreeNode; segments: SegmentMapEntry[]; @@ -37,12 +39,13 @@ export function SegmentsPane({ node, segments }: SegmentsPaneProps) { const layoutPath = row.layout_path as string; const pathNodes = findPathToNode(file.layoutTree, layoutPath); return ( - + {pathNodes.map((pathNode, i) => ( - {i > 0 && /} + {i > 0 && /} )} {size && ( + // The tree is the keyboard path to every block, and a selection there zooms the map, so + // the map takes the pointer only. + // biome-ignore lint/a11y/useKeyWithClickEvents: a pointer shortcut; the tree has the keys. - + + {/* Name sits locally on the block: in a leaf's body, or in a container's header band (which children never occupy, so it cannot collide with them). */} - {label && h > 11 && ( - + {label && h > 16 && ( + {label} )} - {isLeaf && w > 50 && h > 26 && ( - + {isLeaf && w > 64 && h > 32 && ( + {formatBytes(d.bytes)} )} @@ -387,9 +344,15 @@ export function BlockTreemap({ ); } -function truncate(s: string, maxChars: number): string { - if (maxChars < 2) return ''; - return s.length > maxChars ? s.slice(0, maxChars - 1) + '…' : s; +/** The width of one 11px Geist Mono character, which the labels use (vx-label-sm). */ +const CHAR_WIDTH = 6.6; +/** The fewest characters a cut label keeps: a shorter stub ("[25…") says nothing. */ +const MIN_CUT = 8; + +/** The name if it fits, else the name cut with an ellipsis, else nothing. */ +function fitLabel(s: string, maxChars: number): string { + if (s.length <= maxChars) return s; + return maxChars >= MIN_CUT ? `${s.slice(0, maxChars - 1)}…` : ''; } function TileTooltip({ @@ -407,46 +370,20 @@ function TileTooltip({ () => nodePhysicalStats(node, segmentMap, fileSize), [node, segmentMap, fileSize], ); - const dtypeCat = getDtypeCategory(node.dtype); - const dtypeColor = DTYPE_COLORS[dtypeCat]; return ( -
-
- - {getNodeDisplayName(node)} - - - {dtypeCat} - -
-
- rows - - {stats.rowCount.toLocaleString()} - - encoding - - {shortEncoding(node.encoding)} - - data - - {formatBytes(stats.dataBytes)} - - % of file - - {formatPercent(stats.fractionOfFile)} - - density - - {formatBytesPerRow(stats.bytesPerRow)} - -
+
+

+ {getNodeDisplayName(node)} +

+
); } diff --git a/vortex-web/src/components/explorer/DataPreview.tsx b/vortex-web/src/components/explorer/DataPreview.tsx index 16b9c494e45..10c4e142b38 100644 --- a/vortex-web/src/components/explorer/DataPreview.tsx +++ b/vortex-web/src/components/explorer/DataPreview.tsx @@ -76,48 +76,38 @@ export function DataPreview() { try { return decodeArrow(ipcBytes); } catch (err) { - console.error('Arrow decode error:', err); + console.error('Explorer: the Arrow rows did not decode', err); return null; } }, [ipcBytes]); - if (!selectedNodeId) { - return ( -
- Select a node to preview data -
- ); - } - - if (loading) { - return ( -
- Loading preview… -
- ); - } - - if (error) { - return ( -
- {error} -
- ); - } - - if (!decoded || decoded.rows.length === 0) { - return ( -
- No data -
- ); - } + const message = !selectedNodeId + ? 'Select a node to see its rows.' + : loading + ? 'Reading the rows…' + : error + ? 'The rows did not load. Select the node again.' + : !decoded || decoded.rows.length === 0 + ? 'This node has no rows.' + : null; return ( - = ROW_LIMIT} - /> +
+ {message !== null || !decoded ? ( +

+ {message} +

+ ) : ( + = ROW_LIMIT} + /> + )} +
); } diff --git a/vortex-web/src/components/explorer/ExplorerShell.stories.tsx b/vortex-web/src/components/explorer/ExplorerShell.stories.tsx index 993d1722074..6a958d6fd9a 100644 --- a/vortex-web/src/components/explorer/ExplorerShell.stories.tsx +++ b/vortex-web/src/components/explorer/ExplorerShell.stories.tsx @@ -32,7 +32,7 @@ const mockFileState: VortexFileState = { function ExplorerPage() { const [view, setView] = useState('details'); return ( -
+
{}} view={view} onViewChange={setView} /> diff --git a/vortex-web/src/components/explorer/FileDropScreen.tsx b/vortex-web/src/components/explorer/FileDropScreen.tsx index 14db31e0c03..0c7ab1b5370 100644 --- a/vortex-web/src/components/explorer/FileDropScreen.tsx +++ b/vortex-web/src/components/explorer/FileDropScreen.tsx @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -import { useState, useCallback, type DragEvent, type FormEvent } from 'react'; -import { ThemePicker } from '../ThemePicker'; +import { useCallback, useId, useRef, useState, type DragEvent, type FormEvent } from 'react'; +import { Header } from './Header'; interface FileDropScreenProps { onFileLoaded: (file: File) => void; @@ -10,11 +10,15 @@ interface FileDropScreenProps { error: string | null; } +/** The first screen: open a Vortex file from the disk, by drop or by choosing it, or from a URL. */ export function FileDropScreen({ onFileLoaded, loading, error }: FileDropScreenProps) { const [isDragging, setIsDragging] = useState(false); const [url, setUrl] = useState(''); const [fetchingUrl, setFetchingUrl] = useState(false); const [urlError, setUrlError] = useState(null); + const inputRef = useRef(null); + const urlId = useId(); + const errorId = useId(); const handleDragOver = useCallback((e: DragEvent) => { e.preventDefault(); @@ -36,16 +40,14 @@ export function FileDropScreen({ onFileLoaded, loading, error }: FileDropScreenP [onFileLoaded], ); - const handleClick = useCallback(() => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.vortex,.vtx'; - input.onchange = () => { - const file = input.files?.[0]; + const handleChosen = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; if (file) onFileLoaded(file); - }; - input.click(); - }, [onFileLoaded]); + e.target.value = ''; + }, + [onFileLoaded], + ); const handleUrlSubmit = useCallback( async (e: FormEvent) => { @@ -56,14 +58,29 @@ export function FileDropScreen({ onFileLoaded, loading, error }: FileDropScreenP setFetchingUrl(true); setUrlError(null); try { - const resp = await fetch(trimmed); - if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); + let resp: Response; + try { + resp = await fetch(trimmed); + } catch (err) { + // A network fault and a CORS refusal look the same from here. + console.error('Explorer: the fetch failed', err); + setUrlError( + 'The server did not let this page read the file. Download the file, then open it here.', + ); + return; + } + if (!resp.ok) { + setUrlError( + `The file at this address did not load (HTTP ${resp.status}). Check the address.`, + ); + return; + } const blob = await resp.blob(); - const name = trimmed.split('/').pop() ?? 'remote.vortex'; - const file = new File([blob], name, { type: blob.type }); - onFileLoaded(file); + const name = new URL(trimmed).pathname.split('/').pop() || 'remote.vortex'; + onFileLoaded(new File([blob], name, { type: blob.type })); } catch (err) { - setUrlError(err instanceof Error ? err.message : String(err)); + console.error('Explorer: the URL did not open', err); + setUrlError('This address is not valid. Check it and try again.'); } finally { setFetchingUrl(false); } @@ -74,69 +91,98 @@ export function FileDropScreen({ onFileLoaded, loading, error }: FileDropScreenP const busy = loading || fetchingUrl; return ( -
-
- -
-

- Vortex Explorer -

- -
- {busy ? ( -

- {fetchingUrl ? 'Fetching…' : 'Loading…'} -

- ) : ( -
-

- Drop a{' '} - - .vortex - {' '} - file here +

+
+
+
+
+

Open a Vortex file.

+

+ See its layout, encodings, and rows. The file stays on your computer: the Explorer + reads it in this tab.

-

or click to browse

- )} -
- {/* URL input */} -
e.stopPropagation()} - > - setUrl(e.target.value)} - placeholder="https://example.com/file.vortex" - disabled={busy} - className="flex-1 rounded border border-vortex-grey-light/40 dark:border-white/[0.08] bg-transparent px-3 py-1.5 font-mono text-sm text-vortex-fg-light dark:text-vortex-fg placeholder:text-vortex-grey-dark/40 focus:border-vortex-light-blue focus:outline-none disabled:opacity-50" - /> - - + {/* The drop target. Its button is the keyboard and pointer path; the drop is extra. */} + {/* biome-ignore lint/a11y/noStaticElementInteractions: a drop target only; the button inside opens the file. */} +
+

+ {busy + ? fetchingUrl + ? 'Fetching the file…' + : 'Reading the file…' + : 'Drop a .vortex file here'} +

+ + +

.vortex or .vtx

+ {error && ( +
+

+ The file did not open. Open a .vortex or .vtx file. +

+

+ Reason · {error} +

+
+ )} +
- {(error || urlError) && ( -

{urlError || error}

- )} +
+ +
+ setUrl(e.target.value)} + placeholder="https://example.com/orders.vortex" + autoComplete="off" + disabled={busy} + {...(urlError ? { 'aria-invalid': true, 'aria-describedby': errorId } : {})} + /> + +
+ {urlError && ( + + )} + +
+ +
+ No file open +
); } diff --git a/vortex-web/src/components/explorer/FileHeader.tsx b/vortex-web/src/components/explorer/FileHeader.tsx index 2a523dee315..6db48926af4 100644 --- a/vortex-web/src/components/explorer/FileHeader.tsx +++ b/vortex-web/src/components/explorer/FileHeader.tsx @@ -2,7 +2,10 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors import { useVortexFile } from '../../contexts/VortexFileContextCore'; -import { ThemePicker } from '../ThemePicker'; +import { Icon } from '../Icon'; +import { Switch } from '../Switch'; +import { formatBytes, formatRowCount } from '../swimlane/utils'; +import { Header } from './Header'; import type { MainView } from './MainArea'; interface FileHeaderProps { @@ -11,62 +14,51 @@ interface FileHeaderProps { onViewChange: (view: MainView) => void; } +const VIEWS = [ + { value: 'details', label: 'Details' }, + { value: 'swimlane', label: 'Swimlane' }, +] as const; + export function FileHeader({ onClose, view, onViewChange }: FileHeaderProps) { const file = useVortexFile(); return ( -
- - {file.fileName} - - - v{file.version} - -
- {/* Primary view switch — sits with the global controls in the header. */} -
- {(['details', 'swimlane'] as const).map((v) => ( - - ))} -
- -
- - -
-
-
+
+ {file.fileName} + + v{file.version} · {formatBytes(file.fileSize)} · {formatRowCount(file.rowCount)} rows + + + } + tools={} + // Below 640px the switch leaves the row, so the Menu's panel holds its twin. + menu={VIEWS.map((o) => ( + + ))} + end={ + + } + /> ); } diff --git a/vortex-web/src/components/explorer/FileMap.stories.tsx b/vortex-web/src/components/explorer/FileMap.stories.tsx deleted file mode 100644 index 5f4bebf4798..00000000000 --- a/vortex-web/src/components/explorer/FileMap.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { FileMap } from './FileMap'; -import { withMockFileContext, withMockSelection } from '../../storybook/decorators'; -import { ordersMock } from '../../mocks/layouts'; -import { generateSegments } from '../../mocks/segments'; -import { generateFileStructure } from '../../mocks/fileStructure'; -import type { VortexFileState } from '../../contexts/VortexFileContext'; - -const layout = ordersMock(); -const segments = generateSegments(layout, 12_400_000); -const fileStructure = generateFileStructure(segments, 12_400_000); - -const mockFileState: VortexFileState = { - fileName: 'orders.vortex', - fileSize: 12_400_000, - rowCount: 100_000, - version: 1, - dtype: '{order_id=i64, ...}', - layoutTree: layout, - segments, - fileStructure, -}; - -const meta: Meta = { - component: FileMap, - decorators: [withMockFileContext(mockFileState), withMockSelection(layout)], -}; -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { height: 40 }, -}; - -export const Tall: Story = { - args: { height: 80 }, -}; diff --git a/vortex-web/src/components/explorer/FileMap.tsx b/vortex-web/src/components/explorer/FileMap.tsx deleted file mode 100644 index 6f28357af7d..00000000000 --- a/vortex-web/src/components/explorer/FileMap.tsx +++ /dev/null @@ -1,322 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -import { useRef, useEffect, useMemo, useState, useCallback } from 'react'; -import { collectSubtreeSegments, formatBytes } from '../swimlane/utils'; -import { useVortexFile } from '../../contexts/VortexFileContextCore'; -import { useSelection } from '../../contexts/SelectionContextCore'; -import type { SegmentMapEntry } from '../swimlane/types'; - -/** - * Pixel-level byte map of the file. Height matches one line of text (1lh). - * - * Three rendering tiers: - * 1. Selected segment (if any): bright blue, full opacity. - * 2. Other subtree segments: dim blue. - * 3. Everything else: neutral base. - */ -export function FileMap() { - const file = useVortexFile(); - const { state: selection, hoverNode, selectNode } = useSelection(); - const canvasRef = useRef(null); - const containerRef = useRef(null); - const [crosshair, setCrosshair] = useState(null); - const [byteOffset, setByteOffset] = useState(null); - const [hoveredSeg, setHoveredSeg] = useState(null); - - // Segments belonging to the selected subtree, sorted by byte offset - const subtreeSegments = useMemo((): SegmentMapEntry[] => { - if (!selection.selectedNode) return []; - const segIds = new Set(collectSubtreeSegments(selection.selectedNode)); - return file.segments - .filter((s) => segIds.has(s.index)) - .sort((a, b) => a.byteOffset - b.byteOffset); - }, [selection.selectedNode, file.segments]); - - // The single selected segment (if any), sorted for scan - const focusedSegment = useMemo((): SegmentMapEntry | null => { - if (selection.selectedSegmentIndex == null) return null; - return file.segments.find((s) => s.index === selection.selectedSegmentIndex) ?? null; - }, [selection.selectedSegmentIndex, file.segments]); - - // Segments for hover preview (from hovered node or hovered segment) - const hoverSegments = useMemo((): SegmentMapEntry[] => { - if (selection.hoveredNode) { - const segIds = new Set(collectSubtreeSegments(selection.hoveredNode)); - return file.segments - .filter((s) => segIds.has(s.index)) - .sort((a, b) => a.byteOffset - b.byteOffset); - } - if (selection.hoveredSegmentIndex != null) { - const seg = file.segments.find((s) => s.index === selection.hoveredSegmentIndex); - return seg ? [seg] : []; - } - return []; - }, [selection.hoveredNode, selection.hoveredSegmentIndex, file.segments]); - - // Paint - useEffect(() => { - const canvas = canvasRef.current; - const container = containerRef.current; - if (!canvas || !container) return; - - const width = container.clientWidth; - const height = container.clientHeight; - if (width === 0 || height === 0) return; - - const dpr = window.devicePixelRatio || 1; - canvas.width = width * dpr; - canvas.height = height * dpr; - canvas.style.width = `${width}px`; - canvas.style.height = `${height}px`; - - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - const fileSize = file.fileStructure.fileSize; - if (fileSize === 0) return; - - const imgData = ctx.createImageData(width * dpr, height * dpr); - const data = imgData.data; - const imgW = imgData.width; - const imgH = imgData.height; - - const dark = - document.documentElement.classList.contains('dark') || - window.matchMedia('(prefers-color-scheme: dark)').matches; - - // Base: neutral - const baseR = dark ? 50 : 210; - const baseG = dark ? 50 : 210; - const baseB = dark ? 50 : 210; - const baseA = dark ? 80 : 100; - - // Highlight color (vortex light blue #2CB9D1) - const hlR = 44, - hlG = 185, - hlB = 209; - - const hasSubtree = subtreeSegments.length > 0; - const hasHover = hoverSegments.length > 0; - const bytesPerPixel = fileSize / width; - - let segIdx = 0; - let hoverIdx = 0; - - for (let px = 0; px < width; px++) { - const byteStart = px * bytesPerPixel; - const byteEnd = (px + 1) * bytesPerPixel; - - // Advance subtree scan - while ( - segIdx < subtreeSegments.length && - subtreeSegments[segIdx].byteOffset + subtreeSegments[segIdx].byteLength <= byteStart - ) { - segIdx++; - } - - // Advance hover scan - while ( - hoverIdx < hoverSegments.length && - hoverSegments[hoverIdx].byteOffset + hoverSegments[hoverIdx].byteLength <= byteStart - ) { - hoverIdx++; - } - - // Check subtree hit - let isSubtree = false; - if (hasSubtree) { - for (let s = segIdx; s < subtreeSegments.length; s++) { - if (subtreeSegments[s].byteOffset >= byteEnd) break; - isSubtree = true; - break; - } - } - - // Check hover hit - let isHovered = false; - if (hasHover) { - for (let h = hoverIdx; h < hoverSegments.length; h++) { - if (hoverSegments[h].byteOffset >= byteEnd) break; - isHovered = true; - break; - } - } - - // Check focused segment hit - let isFocused = false; - if (focusedSegment) { - const fStart = focusedSegment.byteOffset; - const fEnd = fStart + focusedSegment.byteLength; - if (fStart < byteEnd && fEnd > byteStart) { - isFocused = true; - } - } - - let r: number, g: number, b: number, a: number; - if (isHovered) { - // Hover always wins — bright highlight - r = hlR; - g = hlG; - b = hlB; - a = 255; - } else if (isFocused) { - r = hlR; - g = hlG; - b = hlB; - a = 255; - } else if (isSubtree && !focusedSegment) { - // No segment selected: all subtree segments are bright - r = hlR; - g = hlG; - b = hlB; - a = dark ? 140 : 160; - } else if (isSubtree) { - // A segment is selected: other subtree segments are dimmed - r = hlR; - g = hlG; - b = hlB; - a = dark ? 90 : 110; - } else { - r = baseR; - g = baseG; - b = baseB; - a = baseA; - } - - const dprPxStart = Math.round(px * dpr); - const dprPxEnd = Math.round((px + 1) * dpr); - - for (let dpx = dprPxStart; dpx < dprPxEnd && dpx < imgW; dpx++) { - for (let dy = 0; dy < imgH; dy++) { - const idx = (dy * imgW + dpx) * 4; - data[idx] = r; - data[idx + 1] = g; - data[idx + 2] = b; - data[idx + 3] = a; - } - } - } - - ctx.putImageData(imgData, 0, 0); - }, [file, subtreeSegments, focusedSegment, hoverSegments]); - - // All segments sorted by byte offset for hit-testing - const sortedSegments = useMemo( - () => [...file.segments].sort((a, b) => a.byteOffset - b.byteOffset), - [file.segments], - ); - - // Find the segment at a given byte offset using binary search - const findSegmentAtByte = useCallback( - (byte: number): SegmentMapEntry | null => { - let lo = 0; - let hi = sortedSegments.length - 1; - while (lo <= hi) { - const mid = (lo + hi) >>> 1; - const seg = sortedSegments[mid]; - if (byte < seg.byteOffset) { - hi = mid - 1; - } else if (byte >= seg.byteOffset + seg.byteLength) { - lo = mid + 1; - } else { - return seg; - } - } - return null; - }, - [sortedSegments], - ); - - // Resize observer - useEffect(() => { - const container = containerRef.current; - if (!container) return; - const observer = new ResizeObserver(() => { - const canvas = canvasRef.current; - if (canvas) canvas.dispatchEvent(new Event('resize')); - }); - observer.observe(container); - return () => observer.disconnect(); - }, []); - - const handleMouseMove = useCallback( - (e: React.MouseEvent) => { - const container = containerRef.current; - if (!container) return; - const rect = container.getBoundingClientRect(); - const x = e.clientX - rect.left; - setCrosshair(x); - - const fileSize = file.fileStructure.fileSize; - const byte = (x / container.clientWidth) * fileSize; - setByteOffset(Math.floor(byte)); - - const seg = findSegmentAtByte(byte); - setHoveredSeg(seg); - hoverNode(seg ? seg.layoutPath : null); - }, - [file.fileStructure.fileSize, findSegmentAtByte, hoverNode], - ); - - const handleMouseLeave = useCallback(() => { - setCrosshair(null); - setByteOffset(null); - setHoveredSeg(null); - hoverNode(null); - }, [hoverNode]); - - const handleClick = useCallback( - (e: React.MouseEvent) => { - const container = containerRef.current; - if (!container) return; - const rect = container.getBoundingClientRect(); - const x = e.clientX - rect.left; - const fileSize = file.fileStructure.fileSize; - const byte = (x / container.clientWidth) * fileSize; - const seg = findSegmentAtByte(byte); - if (seg) { - selectNode(seg.layoutPath); - } - }, - [file.fileStructure.fileSize, findSegmentAtByte, selectNode], - ); - - return ( -
- - {crosshair !== null && ( - <> -
- {byteOffset !== null && ( -
- {hoveredSeg ? ( -
- {hoveredSeg.layoutPath} - - segment {hoveredSeg.index} · {formatBytes(hoveredSeg.byteLength)} @{' '} - {formatBytes(hoveredSeg.byteOffset)} - -
- ) : ( - {formatBytes(byteOffset)} - )} -
- )} - - )} -
- ); -} diff --git a/vortex-web/src/components/explorer/Header.tsx b/vortex-web/src/components/explorer/Header.tsx new file mode 100644 index 00000000000..9575452a0a2 --- /dev/null +++ b/vortex-web/src/components/explorer/Header.tsx @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +import type { ReactNode } from 'react'; +import { Icon } from '../Icon'; +import { ThemePicker } from '../ThemePicker'; + +const LINKS = [ + { label: 'Docs', href: 'https://docs.vortex.dev' }, + { label: 'Bench', href: 'https://bench.vortex.dev' }, + { label: 'Explorer', href: '/', current: true }, + { label: 'Blog', href: 'https://vortex.dev/blog' }, + { label: 'Slack', href: 'https://vortex.dev/slack' }, +]; + +function Links() { + return LINKS.map((l) => ( + + {l.label} + + )); +} + +interface HeaderProps { + /** The open file's name and facts, beside the wordmark. */ + file?: ReactNode; + /** Controls for the open file, such as the view switch. With them, the links move to the menu. */ + tools?: ReactNode; + /** Buttons after the theme switch, such as Close. */ + end?: ReactNode; + /** The tools' twin rows in the Menu's panel (vx-header-menu-spill), shown below 640px, where + * the tools leave the row. */ + menu?: ReactNode; +} + +/** The design system's header, as on the other Vortex sites: wordmark, links, and theme. */ +export function Header({ file, tools, end, menu }: HeaderProps) { + return ( +
+
+ + Vortex + Vortex + +

Explorer

+ {file} +
+ + {tools &&
{tools}
} +
+ + {end} +
+ + + + Menu + + +
+
+
+ ); +} diff --git a/vortex-web/src/components/explorer/MainArea.tsx b/vortex-web/src/components/explorer/MainArea.tsx index 8685d68aad2..f7f0fe97a82 100644 --- a/vortex-web/src/components/explorer/MainArea.tsx +++ b/vortex-web/src/components/explorer/MainArea.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useRef, useState, type CSSProperties } from 'react'; import { TreePanel } from './TreePanel'; import { SwimlaneOverview } from '../swimlane/SwimlaneOverview'; import { DataPreview } from './DataPreview'; @@ -9,88 +9,96 @@ import { DetailPanel } from '../detail/DetailPanel'; import { SummarySidebar } from '../detail/SummarySidebar'; const MIN_PANEL_HEIGHT = 120; -const DEFAULT_PREVIEW_HEIGHT = 200; +const DEFAULT_PREVIEW_HEIGHT = 240; +const KEY_STEP = 24; export type MainView = 'details' | 'swimlane'; /** - * Main explorer area: tree panel (left) | the active main view (details or - * swimlane, chosen in the top header) over a resizable data preview (right). The - * tree stays put as column navigation while the header tabs switch the view. - * - * The preview panel at the bottom is vertically resizable via a drag handle. + * The work area: the layout tree (Details view only), then the active view and the summary over + * the data preview. The handle between them sets the preview's height, by drag or with the arrow + * keys. */ export function MainArea({ view }: { view: MainView }) { const [previewHeight, setPreviewHeight] = useState(DEFAULT_PREVIEW_HEIGHT); const dragging = useRef(false); const startY = useRef(0); const startHeight = useRef(0); - const containerRef = useRef(null); + const mainRef = useRef(null); + + // The view and the preview share the room the other rows leave (the handle, and below 1100px + // the summary), so the preview grows only until the view is down to its minimum. + const clamp = useCallback((h: number) => { + const view = mainRef.current?.firstElementChild; + const preview = mainRef.current?.lastElementChild; + const shared = + view && preview + ? view.getBoundingClientRect().height + preview.getBoundingClientRect().height + : 600; + const max = Math.max(MIN_PANEL_HEIGHT, shared - MIN_PANEL_HEIGHT); + return Math.round(Math.min(max, Math.max(MIN_PANEL_HEIGHT, h))); + }, []); const onPointerDown = useCallback( - (e: React.PointerEvent) => { + (e: React.PointerEvent) => { e.preventDefault(); dragging.current = true; startY.current = e.clientY; startHeight.current = previewHeight; - (e.target as HTMLElement).setPointerCapture(e.pointerId); + e.currentTarget.setPointerCapture(e.pointerId); }, [previewHeight], ); - const onPointerMove = useCallback((e: React.PointerEvent) => { - if (!dragging.current) return; - const containerHeight = containerRef.current?.clientHeight ?? 600; - const maxPreview = containerHeight - MIN_PANEL_HEIGHT; - const delta = startY.current - e.clientY; - const next = Math.min(maxPreview, Math.max(MIN_PANEL_HEIGHT, startHeight.current + delta)); - setPreviewHeight(next); - }, []); + const onPointerMove = useCallback( + (e: React.PointerEvent) => { + if (!dragging.current) return; + setPreviewHeight(clamp(startHeight.current + startY.current - e.clientY)); + }, + [clamp], + ); const onPointerUp = useCallback(() => { dragging.current = false; }, []); - return ( -
- {/* Left: tree panel — column navigation paired with the details view. The - swimlane view hides it so the byte-bars get the full width. */} - {view === 'details' && ( -
- -
- )} - - {/* Right: active main view over the resizable data preview */} -
- {/* Active view — fills available vertical space, scrolls internally. The - swimlane keeps the summary sidebar alongside it, like the details view. */} -
- {view === 'details' ? ( - - ) : ( - <> -
- -
- - - )} -
+ const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + const step = { ArrowUp: KEY_STEP, ArrowDown: -KEY_STEP }[e.key]; + if (step === undefined) return; + e.preventDefault(); + setPreviewHeight((h) => clamp(h + step)); + }, + [clamp], + ); - {/* Resize handle */} + return ( +
+ {view === 'details' && } +
+ {view === 'details' ? : } + + {/* A window splitter: the focusable separator pattern of WAI-ARIA. */} + {/* biome-ignore lint/a11y/useSemanticElements: an hr cannot take focus or a drag. */}
- - {/* Data preview — resizable bottom section */} -
- -
+
-
+
); } diff --git a/vortex-web/src/components/explorer/StatusBar.tsx b/vortex-web/src/components/explorer/StatusBar.tsx index 5ec4ff276f1..eb3037369fa 100644 --- a/vortex-web/src/components/explorer/StatusBar.tsx +++ b/vortex-web/src/components/explorer/StatusBar.tsx @@ -4,63 +4,43 @@ import { useMemo } from 'react'; import { useVortexFile } from '../../contexts/VortexFileContextCore'; import { useSelection } from '../../contexts/SelectionContextCore'; -import { formatBytes, formatRowCount, collectSubtreeSegments } from '../swimlane/utils'; - +import { + formatBytes, + formatRowCount, + collectSubtreeSegments, + getNodeDisplayName, +} from '../swimlane/utils'; + +/** The status line: where the file is read, the file's totals, then the hovered or selected node's. */ export function StatusBar() { const file = useVortexFile(); const { state: selection } = useSelection(); - // Show hovered node stats if hovering, otherwise selected node stats. const activeNode = selection.hoveredNode ?? selection.selectedNode; const selectionStats = useMemo(() => { if (!activeNode) return null; - const segIds = new Set(collectSubtreeSegments(activeNode)); const reachable = file.segments.filter((s) => segIds.has(s.index)); - const totalBytes = reachable.reduce((sum, s) => sum + s.byteLength, 0); - const pct = - file.fileStructure.fileSize > 0 ? (totalBytes / file.fileStructure.fileSize) * 100 : 0; - - return { - rows: activeNode.rowCount, - segments: reachable.length, - bytes: totalBytes, - pct, - }; + const bytes = reachable.reduce((sum, s) => sum + s.byteLength, 0); + const pct = file.fileStructure.fileSize > 0 ? (bytes / file.fileStructure.fileSize) * 100 : 0; + return { rows: activeNode.rowCount, segments: reachable.length, bytes, pct }; }, [activeNode, file.segments, file.fileStructure.fileSize]); return ( -
- {/* File-level stats — left */} -
- - - -
- -
- - {/* Selection stats — right */} - {selectionStats && ( -
- - - -
+
+ Read in your browser · nothing uploaded + + {formatRowCount(file.rowCount)} rows · {formatBytes(file.fileSize)} · {file.segments.length}{' '} + segments + + {activeNode && selectionStats && ( + + {getNodeDisplayName(activeNode)} · {formatRowCount(selectionStats.rows)} rows ·{' '} + {selectionStats.segments} segments · {formatBytes(selectionStats.bytes)} ( + {selectionStats.pct.toFixed(1)}%) + )} -
- ); -} - -function Stat({ label, value }: { label: string; value: string }) { - return ( - - {label}: - {value} - + ); } diff --git a/vortex-web/src/components/explorer/TreePanel.tsx b/vortex-web/src/components/explorer/TreePanel.tsx index 77819261a99..64f1b000370 100644 --- a/vortex-web/src/components/explorer/TreePanel.tsx +++ b/vortex-web/src/components/explorer/TreePanel.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; +import { useState, useCallback, useMemo, useEffect, useRef, type KeyboardEvent } from 'react'; import { useVortexFile } from '../../contexts/VortexFileContextCore'; import { useSelection } from '../../contexts/SelectionContextCore'; import { @@ -10,12 +10,22 @@ import { findPathToNode, isFlatLayout, findNodeById, + formatBytes, + collectSubtreeSegments, + hasExpandableChildren, } from '../swimlane/utils'; +import { arraySubtreeBytes } from './physicalStats'; +import { Switch } from '../Switch'; import { TreeRow } from '../swimlane/TreeRow'; import { TreeSearch } from '../swimlane/TreeSearch'; type TreeMode = 'schema' | 'layout'; +const MODES = [ + { value: 'schema', label: 'Schema' }, + { value: 'layout', label: 'Layout' }, +] as const; + export function TreePanel() { const file = useVortexFile(); const { state: selection, selectNode, hoverNode } = useSelection(); @@ -123,54 +133,140 @@ export function TreePanel() { [selection.selectedNodeId, selectNode], ); - return ( -
- {/* Header: mode toggle + search */} -
- -
- -
-
+ // Each node's data size, for the rows that show. + const segmentBytes = useMemo( + () => new Map(file.segments.map((s) => [s.index, s.byteLength])), + [file.segments], + ); + const sizes = useMemo(() => { + const out = new Map(); + for (const { node, displayKind } of visibleRows) { + if (displayKind === 'hiddenIndicator') continue; + const bytes = node.isArrayNode + ? arraySubtreeBytes(node) + : [...new Set(collectSubtreeSegments(node))].reduce( + (sum, id) => sum + (segmentBytes.get(id) ?? 0), + 0, + ); + out.set(node.id, formatBytes(bytes)); + } + return out; + }, [visibleRows, segmentBytes]); - {/* Tree rows */} -
- {visibleRows.map((row) => ( - toggleExpanded(row.node.id)} - onSelect={() => handleNodeClick(row.node.id)} - onHover={hoverNode} - /> - ))} -
-
+ // The tree is one tab stop: the selected node, else the first. + const focusId = + visibleRows.find((r) => r.node.id === selection.selectedNodeId)?.node.id ?? + visibleRows[0]?.node.id; + + const focusNode = useCallback((id: string) => { + requestAnimationFrame(() => { + scrollContainerRef.current + ?.querySelector(`[data-node-id="${CSS.escape(id)}"]`) + ?.focus(); + }); + }, []); + + // The keys of the WAI-ARIA tree pattern, as the design system's treeKey gives them. Every row + // drawn is visible, so Up and Down step through the rows. + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + const el = (e.target as Element).closest('[data-node-id]'); + const at = visibleRows.findIndex((r) => r.node.id === el?.dataset.nodeId); + const row = visibleRows[at]; + if (!row) return; + const go = (i: number) => { + const target = visibleRows[Math.max(0, Math.min(visibleRows.length - 1, i))]; + if (!target) return; + // A group row ("···") and the hidden-rows line are not nodes of the file: focus them only. + if (target.displayKind === 'normal') selectNode(target.node.id); + focusNode(target.node.id); + }; + const parent = row.displayKind !== 'hiddenIndicator' && hasExpandableChildren(row.node); + const open = expanded.has(row.node.id); + switch (e.key) { + case 'ArrowDown': + go(at + 1); + break; + case 'ArrowUp': + go(at - 1); + break; + case 'Home': + go(0); + break; + case 'End': + go(visibleRows.length - 1); + break; + case 'ArrowRight': + if (!parent) return; + if (!open) toggleExpanded(row.node.id); + else if ((visibleRows[at + 1]?.depth ?? -1) > row.depth) go(at + 1); + break; + case 'ArrowLeft': { + if (parent && open) { + toggleExpanded(row.node.id); + break; + } + let j = at - 1; + while (j >= 0 && (visibleRows[j]?.depth ?? 0) >= row.depth) j--; + if (j < 0) return; + go(j); + break; + } + case 'Enter': + case ' ': + if (!parent) return; + toggleExpanded(row.node.id); + break; + default: + return; + } + e.preventDefault(); + }, + [visibleRows, expanded, selectNode, focusNode, toggleExpanded], ); -} -/** Subtle segmented toggle for Schema / Layout mode */ -function ModeToggle({ mode, onChange }: { mode: TreeMode; onChange: (m: TreeMode) => void }) { return ( -
- {(['schema', 'layout'] as const).map((m) => ( - - ))} -
+ {visibleRows.map((row) => ( + toggleExpanded(row.node.id)} + onSelect={() => + row.displayKind === 'group' + ? toggleExpanded(row.node.id) + : handleNodeClick(row.node.id) + } + onHover={hoverNode} + {...(sizes.has(row.node.id) ? { size: sizes.get(row.node.id)! } : {})} + /> + ))} +
+
+
+ {visibleRows.length} {visibleRows.length === 1 ? 'node' : 'nodes'} shown + {searchQuery ? ` for “${searchQuery}”` : ''} +
+ ); } diff --git a/vortex-web/src/components/explorer/physicalStats.ts b/vortex-web/src/components/explorer/physicalStats.ts index 6df674bea3f..d9424313ca7 100644 --- a/vortex-web/src/components/explorer/physicalStats.ts +++ b/vortex-web/src/components/explorer/physicalStats.ts @@ -105,7 +105,7 @@ export function formatPercent(fraction: number): string { /** Format bytes-per-row density, e.g. "4.0 B/row" or "—" when unknown. */ export function formatBytesPerRow(bytesPerRow: number | null): string { if (bytesPerRow === null) return '—'; - if (bytesPerRow >= 1024) return `${(bytesPerRow / 1024).toFixed(1)} KB/row`; + if (bytesPerRow >= 1e3) return `${(bytesPerRow / 1e3).toFixed(1)} KB/row`; if (bytesPerRow >= 10) return `${bytesPerRow.toFixed(0)} B/row`; return `${bytesPerRow.toFixed(2)} B/row`; } diff --git a/vortex-web/src/components/swimlane/AxisBar.tsx b/vortex-web/src/components/swimlane/AxisBar.tsx deleted file mode 100644 index 3167a185593..00000000000 --- a/vortex-web/src/components/swimlane/AxisBar.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -import { useMemo } from 'react'; -import { formatBytes } from './utils'; - -interface AxisBarProps { - totalBytes: number; - swimlaneMinWidth: number; - rulerPosition: { x: number; byte: number } | null; - scrollLeft: number; - containerWidth: number; - axisRef: React.RefObject; -} - -export function AxisBar({ - totalBytes, - swimlaneMinWidth, - rulerPosition, - scrollLeft, - containerWidth, - axisRef, -}: AxisBarProps) { - const axisTicks = useMemo(() => { - const ticks = []; - const step = totalBytes / 5; - for (let i = 0; i <= 5; i++) { - ticks.push(Math.round(i * step)); - } - return ticks; - }, [totalBytes]); - - return ( - // Reserve the same scrollbar gutter as the swimlane panel so the ticks stay - // aligned with the bars whether or not the vertical scrollbar is present. -
-
- {axisTicks.map((tick) => ( -
- {formatBytes(tick)} -
- ))} -
- - {rulerPosition && ( -
- {formatBytes(rulerPosition.byte)} -
- )} -
- ); -} diff --git a/vortex-web/src/components/swimlane/DtypeLegend.tsx b/vortex-web/src/components/swimlane/DtypeLegend.tsx deleted file mode 100644 index 11ca38292b9..00000000000 --- a/vortex-web/src/components/swimlane/DtypeLegend.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -import { DTYPE_CATEGORIES, DTYPE_COLORS } from './styles'; - -export function DtypeLegend() { - return ( -
- {DTYPE_CATEGORIES.map((cat) => ( -
-
- {cat} -
- ))} -
- ); -} diff --git a/vortex-web/src/components/swimlane/LayoutSwimlane.stories.tsx b/vortex-web/src/components/swimlane/LayoutSwimlane.stories.tsx deleted file mode 100644 index 1df8c893ea7..00000000000 --- a/vortex-web/src/components/swimlane/LayoutSwimlane.stories.tsx +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -import { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { LayoutSwimlane } from './LayoutSwimlane'; -import type { LayoutTreeNode } from './types'; -import { - ordersMock, - simpleMock, - wideMock, - deepMock, - heavyChunksMock, - gappedMock, - generateSegments, -} from '../../mocks'; - -/** Bundle a layout with a generated physical segment map for the byte-based axis. */ -function physical(layout: LayoutTreeNode, fileSize: number) { - return { layout, segments: generateSegments(layout, fileSize), totalBytes: fileSize }; -} - -const meta: Meta = { - component: LayoutSwimlane, - parameters: { - layout: 'fullscreen', - }, - decorators: [ - (Story) => ( -
- -
- ), - ], -}; -export default meta; - -type Story = StoryObj; - -const ordersLayout = ordersMock(); - -export const Orders: Story = { - args: { - ...physical(ordersLayout, 12_400_000), - defaultExpanded: ['root', 'root.customer', 'root.customer.id', 'root.status'], - height: 400, - }, -}; - -export const SchemaMode: Story = { - args: { - ...physical(ordersLayout, 12_400_000), - mode: 'schema', - defaultExpanded: ['root', 'root.customer'], - height: 400, - }, -}; - -export const LayoutMode: Story = { - args: { - ...physical(ordersLayout, 12_400_000), - mode: 'layout', - defaultExpanded: ['root', 'root.customer', 'root.customer.id', 'root.status'], - height: 400, - }, -}; - -export const SingleFlat: Story = { - args: { - ...physical(simpleMock(), 800_000), - height: 100, - }, -}; - -export const WideSchema: Story = { - args: { - ...physical(wideMock(), 6_000_000), - mode: 'schema', - defaultExpanded: ['root'], - height: 400, - }, -}; - -export const DeepNesting: Story = { - args: { - ...physical(deepMock(), 2_500_000), - mode: 'schema', - defaultExpanded: ['root', 'root.user', 'root.user.profile', 'root.user.profile.address'], - height: 400, - }, -}; - -export const HeavyChunks: Story = { - args: { - ...physical(heavyChunksMock(), 60_000_000), - mode: 'layout', - defaultExpanded: ['root', 'root.values'], - height: 400, - }, -}; - -/** Two columns interleaved on disk: each column's chunks are split by the other's, - so plotting by physical offset reveals the gaps in each column's storage. */ -export const Gapped: Story = { - args: { - ...gappedMock(), - mode: 'schema', - defaultExpanded: ['root', 'root.temp', 'root.count'], - height: 240, - }, -}; - -/** Interactive story demonstrating controlled selection */ -export const WithSelection: StoryObj = { - render: () => { - const [selectedId, setSelectedId] = useState(null); - return ( -
-
Selected: {selectedId ?? 'none'}
- -
- ); - }, -}; diff --git a/vortex-web/src/components/swimlane/LayoutSwimlane.tsx b/vortex-web/src/components/swimlane/LayoutSwimlane.tsx deleted file mode 100644 index 823a81784fd..00000000000 --- a/vortex-web/src/components/swimlane/LayoutSwimlane.tsx +++ /dev/null @@ -1,274 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; -import type { LayoutTreeNode, FlattenedRow, SegmentMapEntry, PhysicalSegment } from './types'; -import { flattenTree, filterTreeBySearch, buildSegmentIndex } from './utils'; -import { ROW_HEIGHT, TREE_WIDTH, DEFAULT_SWIMLANE_MIN_WIDTH } from './styles'; -import { TreeRow } from './TreeRow'; -import { TreeSearch } from './TreeSearch'; -import { SwimlaneBar } from './SwimlaneBar'; -import { AxisBar } from './AxisBar'; -import { Tooltip } from './Tooltip'; - -export interface LayoutSwimlaneProps { - /** The root layout tree node to visualize */ - layout: LayoutTreeNode; - /** Physical segment map for the file (byte offsets and lengths). */ - segments: SegmentMapEntry[]; - /** Total byte span of the file — the x-axis maximum. */ - totalBytes: number; - /** Initially expanded node IDs */ - defaultExpanded?: string[]; - /** Currently selected node ID (controlled) */ - selectedNodeId?: string | null; - /** Callback when a tree/bar node is clicked */ - onNodeSelect?: (nodeId: string | null) => void; - /** Display mode: 'schema' shows logical columns, 'layout' shows full layout tree */ - mode?: 'schema' | 'layout'; - /** Minimum width of the swimlane panel */ - swimlaneMinWidth?: number; - /** Height of the scrollable area */ - height?: number; -} - -export function LayoutSwimlane({ - layout, - segments, - totalBytes, - defaultExpanded = [], - selectedNodeId = null, - onNodeSelect, - mode = 'schema', - swimlaneMinWidth = DEFAULT_SWIMLANE_MIN_WIDTH, - height, -}: LayoutSwimlaneProps) { - const [expanded, setExpanded] = useState>(() => new Set(defaultExpanded)); - const [searchQuery, setSearchQuery] = useState(''); - const [tooltip, setTooltip] = useState<{ - node: LayoutTreeNode; - position: { x: number; y: number }; - } | null>(null); - const [rulerPosition, setRulerPosition] = useState<{ x: number; byte: number } | null>(null); - - const treeScrollRef = useRef(null); - const swimlaneScrollRef = useRef(null); - const swimlanePanelRef = useRef(null); - const axisRef = useRef(null); - - // Flatten the tree - const allRows = useMemo( - () => flattenTree(layout, expanded, null, mode), - [layout, expanded, mode], - ); - - const visibleRows = useMemo( - () => filterTreeBySearch(allRows, searchQuery, layout), - [allRows, searchQuery, layout], - ); - - // Resolve each segment to its physical byte placement so bars can be plotted - // by file offset rather than by row. - const segmentIndex = useMemo(() => buildSegmentIndex(layout, segments), [layout, segments]); - - const toggleExpanded = useCallback((id: string) => { - setExpanded((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }, []); - - const handleNodeClick = useCallback( - (nodeId: string) => { - onNodeSelect?.(selectedNodeId === nodeId ? null : nodeId); - }, - [selectedNodeId, onNodeSelect], - ); - - const handleTooltip = useCallback( - (node: LayoutTreeNode | null, position: { x: number; y: number }) => { - setTooltip(node ? { node, position } : null); - }, - [], - ); - - // Sync vertical scroll between tree and swimlane - useEffect(() => { - const tree = treeScrollRef.current; - const swimlane = swimlaneScrollRef.current; - if (!tree || !swimlane) return; - - let syncing = false; - const syncScroll = (source: HTMLDivElement, target: HTMLDivElement) => () => { - if (syncing) return; - syncing = true; - target.scrollTop = source.scrollTop; - syncing = false; - }; - - const treeHandler = syncScroll(tree, swimlane); - const swimlaneHandler = syncScroll(swimlane, tree); - tree.addEventListener('scroll', treeHandler); - swimlane.addEventListener('scroll', swimlaneHandler); - return () => { - tree.removeEventListener('scroll', treeHandler); - swimlane.removeEventListener('scroll', swimlaneHandler); - }; - }, []); - - // Sync horizontal scroll between swimlane and axis - useEffect(() => { - const swimlane = swimlaneScrollRef.current; - const axis = axisRef.current; - if (!swimlane || !axis) return; - - const handleScroll = () => { - axis.style.transform = `translateX(-${swimlane.scrollLeft}px)`; - }; - swimlane.addEventListener('scroll', handleScroll); - return () => swimlane.removeEventListener('scroll', handleScroll); - }, []); - - // Ruler mouse tracking - const handleSwimlaneMouseMove = useCallback( - (e: React.MouseEvent) => { - const panel = swimlanePanelRef.current; - if (!panel) return; - const rect = panel.getBoundingClientRect(); - const x = e.clientX - rect.left; - const panelWidth = panel.offsetWidth; - if (x >= 0 && x <= panelWidth) { - const byte = (x / panelWidth) * totalBytes; - setRulerPosition({ x, byte: Math.max(0, Math.min(totalBytes, byte)) }); - } - }, - [totalBytes], - ); - - const handleSwimlaneMouseLeave = useCallback(() => setRulerPosition(null), []); - - const contentHeight = visibleRows.length * ROW_HEIGHT; - - return ( -
- {/* Filter header — sits above the tree column only, so the tree rows and - swimlane bars below it start at the same vertical offset and stay aligned. */} -
-
- -
-
-
- - {/* Tree + Swimlane */} -
- {/* Tree panel */} -
-
-
- {visibleRows.map((row) => ( - toggleExpanded(row.node.id)} - onSelect={() => handleNodeClick(row.node.id)} - /> - ))} -
-
-
- - {/* Swimlane panel. scrollbar-gutter reserves space for the vertical - scrollbar so bars never overflow underneath it, and the inner panel is - the positioning context so bar widths track the content (not the - scrollbar) and stay aligned with the axis. */} -
-
- {visibleRows.map((row) => ( - handleNodeClick(row.node.id)} - isSelected={selectedNodeId === row.node.id} - /> - ))} - - {rulerPosition && ( -
- )} -
-
-
- - {/* Axis */} -
-
- -
- - {tooltip && } -
- ); -} - -/** A single swimlane row — just height + bar positioning, no decoration */ -function SwimlaneRow({ - row, - totalBytes, - segmentIndex, - onHover, - onSelect, - isSelected, -}: { - row: FlattenedRow; - totalBytes: number; - segmentIndex: Map; - onHover: (node: LayoutTreeNode | null, position: { x: number; y: number }) => void; - onSelect: () => void; - isSelected: boolean; -}) { - return ( -
- {row.displayKind !== 'hiddenIndicator' && ( - - )} -
- ); -} - -export default LayoutSwimlane; diff --git a/vortex-web/src/components/swimlane/SplitRegion.tsx b/vortex-web/src/components/swimlane/SplitRegion.tsx deleted file mode 100644 index b882efb2506..00000000000 --- a/vortex-web/src/components/swimlane/SplitRegion.tsx +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -import React, { useState } from 'react'; -import type { Split } from './types'; -import { MIN_LABEL_WIDTH } from './styles'; - -interface SplitRegionProps { - split: Split; - totalRows: number; - swimlaneWidth: number; - isSelected: boolean; - onClick: (e: React.MouseEvent) => void; -} - -export function SplitRegion({ - split, - totalRows, - swimlaneWidth, - isSelected, - onClick, -}: SplitRegionProps) { - const [isHovered, setIsHovered] = useState(false); - - const left = (split.rowRange[0] / totalRows) * 100; - const width = ((split.rowRange[1] - split.rowRange[0]) / totalRows) * 100; - const widthPx = ((split.rowRange[1] - split.rowRange[0]) / totalRows) * swimlaneWidth; - const showLabel = widthPx >= MIN_LABEL_WIDTH || isSelected || isHovered; - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > -
- {split.id} -
-
- ); -} diff --git a/vortex-web/src/components/swimlane/SwimlaneBar.tsx b/vortex-web/src/components/swimlane/SwimlaneBar.tsx index 4422527cd1f..39c8b0c1b9d 100644 --- a/vortex-web/src/components/swimlane/SwimlaneBar.tsx +++ b/vortex-web/src/components/swimlane/SwimlaneBar.tsx @@ -1,10 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -import React from 'react'; +import type React from 'react'; import type { LayoutTreeNode, FlattenedRow, PhysicalSegment } from './types'; -import { collectSubtreeSegments, getDtypeCategory, MIN_CELL_WIDTH } from './utils'; -import { DTYPE_COLORS, getEncodingStyle } from './styles'; +import { collectSubtreeSegments, dtypeClass, getNodeDisplayName } from './utils'; interface SwimlaneBarProps { row: FlattenedRow; @@ -15,14 +14,16 @@ interface SwimlaneBarProps { onHover: (node: LayoutTreeNode | null, position: { x: number; y: number }) => void; /** Focus this node's column when its bar is clicked. */ onSelect: () => void; - /** Whether this node's column is the current focused selection. */ - isSelected: boolean; } +const pct = (n: number, total: number) => `${(n / total) * 100}%`; + /** - * A swimlane bar plotted by physical byte offset. Every segment reachable from - * the node is drawn at its real position in the file, so a column whose storage - * is split across the file (interleaved with other columns) shows visible gaps. + * A swimlane bar plotted by physical byte offset. Every segment reachable from the node is drawn + * at its real position in the file, in its dtype's colour, so a column whose storage is split + * across the file shows gaps. Each contiguous run sits in a frame you can click; the frame takes + * the accent when the column is selected (from the lane's `is-selected`). The lane's name is the + * keyboard path, so the frames are not tab stops. */ export function SwimlaneBar({ row, @@ -30,68 +31,49 @@ export function SwimlaneBar({ segmentIndex, onHover, onSelect, - isSelected, }: SwimlaneBarProps) { const { node } = row; - const style = getEncodingStyle(node.encoding); - if (totalBytes <= 0) return null; - // Physical segments reachable from this node, in file order. const segments = collectSubtreeSegments(node) .map((id) => segmentIndex.get(id)) .filter((s): s is PhysicalSegment => s != null) .sort((a, b) => a.byteOffset - b.byteOffset); - if (segments.length === 0) return null; - // Merge into contiguous runs for the encoding frame. Gaps between runs are - // physical gaps in the column's representation and are left empty. + // Merge into contiguous runs. Gaps between runs are physical gaps and stay empty. const runs: Array<[number, number]> = []; for (const seg of segments) { - const start = seg.byteOffset; const end = seg.byteOffset + seg.byteLength; const last = runs[runs.length - 1]; - if (last && start <= last[1]) last[1] = Math.max(last[1], end); - else runs.push([start, end]); + if (last && seg.byteOffset <= last[1]) last[1] = Math.max(last[1], end); + else runs.push([seg.byteOffset, end]); } - const handleEnter = (e: React.MouseEvent) => onHover(node, { x: e.clientX, y: e.clientY }); - const handleMove = (e: React.MouseEvent) => onHover(node, { x: e.clientX, y: e.clientY }); - const handleLeave = () => onHover(null, { x: 0, y: 0 }); + const hover = (e: React.PointerEvent) => onHover(node, { x: e.clientX, y: e.clientY }); + const leave = () => onHover(null, { x: 0, y: 0 }); + const name = getNodeDisplayName(node); return ( <> - {segments.map((seg) => { - const color = DTYPE_COLORS[getDtypeCategory(seg.dtype)]; - return ( -
- ); - })} - {runs.map(([start, end]) => ( -
( + + ))} + {runs.map(([start, end], i) => ( + + - - {getNodeDisplayName(node)} -
- -
- toggleSelect(node.id)} - isSelected={isSelected} - /> -
-
- ); - })} -
- - {/* Byte axis — aligned to the bars column via the same label-width offset and - scrollbar gutter. */} -
-
-
- {axisTicks.map((tick) => ( -
- {formatBytes(tick)} -
- ))} + + toggleSelect(node.id)} + /> + + + ); + })} + + +
{tooltip && } -
+ ); } diff --git a/vortex-web/src/components/swimlane/Tooltip.tsx b/vortex-web/src/components/swimlane/Tooltip.tsx index 45aa1c78600..c71cc2b63f4 100644 --- a/vortex-web/src/components/swimlane/Tooltip.tsx +++ b/vortex-web/src/components/swimlane/Tooltip.tsx @@ -1,68 +1,35 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +import { Facts } from '../detail/Facts'; import type { LayoutTreeNode } from './types'; import { - getDtypeCategory, + dtypeClass, formatBytes, getNodeDisplayName, getNodeRowRange, shortEncoding, } from './utils'; -import { DTYPE_COLORS } from './styles'; interface TooltipProps { node: LayoutTreeNode; position: { x: number; y: number }; } +/** The card that follows the pointer over a swimlane bar. */ export function Tooltip({ node, position }: TooltipProps) { - const rowRange = getNodeRowRange(node); - const rows = rowRange[1] - rowRange[0]; - const dtypeCat = getDtypeCategory(node.dtype); - const dtypeColor = DTYPE_COLORS[dtypeCat]; - const name = getNodeDisplayName(node); + const [start, end] = getNodeRowRange(node); + const items: [string, string, string?][] = [['Rows', (end - start).toLocaleString('en-US')]]; + if (node.dtype) items.push(['Dtype', node.dtype]); + if (node.encoding) items.push(['Encoding', shortEncoding(node.encoding), node.encoding]); + if (node.metadataBytes > 0) items.push(['Metadata', formatBytes(node.metadataBytes)]); return ( -
-
- {name} - - {dtypeCat} - -
-
- rows - {rows.toLocaleString()} - {node.dtype && ( - <> - dtype - {node.dtype} - - )} - {node.encoding && ( - <> - encoding - - {shortEncoding(node.encoding)} - - - )} - {node.metadataBytes > 0 && ( - <> - metadata - - {formatBytes(node.metadataBytes)} - - - )} -
+
+

+ {getNodeDisplayName(node)} +

+
); } diff --git a/vortex-web/src/components/swimlane/TreeRow.tsx b/vortex-web/src/components/swimlane/TreeRow.tsx index 37f5d845c7f..29d9138395f 100644 --- a/vortex-web/src/components/swimlane/TreeRow.tsx +++ b/vortex-web/src/components/swimlane/TreeRow.tsx @@ -1,10 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +import type { CSSProperties } from 'react'; import type { FlattenedRow } from './types'; +import { Icon } from '../Icon'; import { - ROW_HEIGHT, - DTYPE_COLORS, + dtypeClass, getEncodingStyle, getNodeDisplayName, hasExpandableChildren, @@ -20,11 +21,36 @@ interface TreeRowProps { isHovered?: boolean; isHoveredAncestor?: boolean; mode: 'schema' | 'layout'; + /** The node's data size, for the right column. */ + size?: string; + /** 0 for the one node in the tab order, -1 for the rest. */ + tabIndex?: number; onToggle: () => void; onSelect: () => void; onHover?: (nodeId: string | null) => void; } +/** What the type column says: the dtype in Schema mode, the layout or row range in Layout mode. */ +function typeText(row: FlattenedRow, mode: 'schema' | 'layout'): string { + const { node, displayKind } = row; + if (displayKind === 'group') return '···'; + if (mode === 'schema') { + if (getDtypeCategory(node.dtype) === 'struct') return 'struct'; + const d = shortEncoding(node.dtype); + return d.length > 24 ? `${d.slice(0, 22)}…` : d; + } + if (node.childType.kind === 'chunk') return formatRowRange(row.rowRange); + const label = getEncodingStyle(node.encoding).label; + return node.children.length > 0 && node.childType.kind !== 'field' + ? `${label} · ${node.children.length}` + : label; +} + +/** + * One node of the layout tree: a design system `vx-tree-node` with the tree pattern's roles. The + * parent tree (`role="tree"`) owns the keys; a click selects, and a click on the chevron opens or + * closes the node. + */ export function TreeRow({ row, isExpanded, @@ -32,102 +58,75 @@ export function TreeRow({ isHovered, isHoveredAncestor, mode, + size, + tabIndex = -1, onToggle, onSelect, onHover, }: TreeRowProps) { const { node, depth, displayKind } = row; + const depthStyle = { '--vx-depth': depth } as CSSProperties; if (displayKind === 'hiddenIndicator') { const name = node.childType.kind === 'transparent' ? node.childType.name : 'hidden'; return (
- {name} + + + {name} +
); } - const isGroup = displayKind === 'group'; const expandable = hasExpandableChildren(node); - const isLeaf = !expandable; - const style = getEncodingStyle(node.encoding); - const name = getNodeDisplayName(node); - - let badgeText: string; - let badgeColor: string; - if (isGroup) { - badgeText = '···'; - badgeColor = style.color; - } else if (mode === 'schema') { - const dtypeCat = getDtypeCategory(node.dtype); - const dtypeStr = shortEncoding(node.dtype); - badgeText = - dtypeCat === 'struct' - ? 'struct' - : dtypeStr.length > 20 - ? dtypeStr.slice(0, 18) + '…' - : dtypeStr; - badgeColor = DTYPE_COLORS[dtypeCat]; - } else { - const ct = node.childType; - if (ct.kind === 'chunk') { - badgeText = formatRowRange(row.rowRange); - } else { - badgeText = style.label; - if (node.children.length > 0 && node.childType.kind !== 'field') { - badgeText += ` (${node.children.length})`; - } - } - badgeColor = style.color; - } - - const opacity = isGroup ? 'opacity-50' : isLeaf ? 'opacity-70' : ''; - const fontStyle = isGroup ? 'italic' : ''; - const highlightBg = isSelected - ? 'bg-vortex-light-blue/10' - : isHovered - ? 'bg-vortex-light-blue/15' - : isHoveredAncestor - ? 'bg-vortex-light-blue/5' - : ''; + const classes = [ + 'vx-tree-node', + displayKind === 'group' && 'is-group', + node.isArrayNode && 'is-array', + isHovered && !isSelected && 'is-hovered', + isHoveredAncestor && !isSelected && 'is-hovered-parent', + ] + .filter(Boolean) + .join(' '); return ( + // The tree (role="tree") takes the keys for all its nodes, as the WAI-ARIA tree pattern does. + // biome-ignore lint/a11y/useKeyWithClickEvents: the tree handles the keys.
{ + // A click on the chevron opens or closes the node and leaves the selection as it is. + if (expandable && (e.target as Element).closest('.vx-tree-toggle')) onToggle(); + else onSelect(); }} - onClick={onSelect} onMouseEnter={() => onHover?.(node.id)} onMouseLeave={() => onHover?.(null)} > - { - e.stopPropagation(); - if (expandable) onToggle(); - }} - > - {isExpanded ? '▼' : '▶'} - - - {name} - - - {badgeText} + + + + {getNodeDisplayName(node)} + {typeText(row, mode)} + {size && {size}}
); } diff --git a/vortex-web/src/components/swimlane/TreeSearch.tsx b/vortex-web/src/components/swimlane/TreeSearch.tsx index b02613db054..ab2c5c33376 100644 --- a/vortex-web/src/components/swimlane/TreeSearch.tsx +++ b/vortex-web/src/components/swimlane/TreeSearch.tsx @@ -1,47 +1,40 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -import { useState, useCallback } from 'react'; +import { useCallback, useId, useState } from 'react'; interface TreeSearchProps { onSearch: (query: string) => void; } +/** A search field that filters the tree. Escape clears it, as in any search field. */ export function TreeSearch({ onSearch }: TreeSearchProps) { const [value, setValue] = useState(''); + const id = useId(); const handleChange = useCallback( (e: React.ChangeEvent) => { - const q = e.target.value; - setValue(q); - onSearch(q); + setValue(e.target.value); + onSearch(e.target.value); }, [onSearch], ); - const handleClear = useCallback(() => { - setValue(''); - onSearch(''); - }, [onSearch]); - return ( -
- ⌕ + <> + - {value && ( - - )} -
+ ); } diff --git a/vortex-web/src/components/swimlane/index.ts b/vortex-web/src/components/swimlane/index.ts deleted file mode 100644 index 893b6ce2f66..00000000000 --- a/vortex-web/src/components/swimlane/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -// Main component -export { LayoutSwimlane, default } from './LayoutSwimlane'; -export type { LayoutSwimlaneProps } from './LayoutSwimlane'; - -// Types -export type { - LayoutTreeNode, - LayoutChildKind, - SegmentMapEntry, - FileStructureInfo, - FlattenedRow, - DisplayKind, - Split, - DtypeCategory, -} from './types'; - -// Sub-components -export { TreeRow } from './TreeRow'; -export { TreeSearch } from './TreeSearch'; -export { SwimlaneBar } from './SwimlaneBar'; -export { AxisBar } from './AxisBar'; -export { Tooltip } from './Tooltip'; -export { DtypeLegend } from './DtypeLegend'; -export { SplitRegion } from './SplitRegion'; - -// Utilities -export { - getDtypeCategory, - rangesOverlap, - createSplits, - formatBytes, - formatRowRange, - formatRowCount, - getNodeDisplayName, - getNodeRowRange, - getEncodingStyle, - hasExpandableChildren, - flattenTree, - filterTreeBySearch, - findNodeById, - findPathToNode, - collectSubtreeIds, - collectSubtreeSegments, - ENCODING_STYLES, - DTYPE_COLORS, - DTYPE_CATEGORIES, -} from './utils'; diff --git a/vortex-web/src/components/swimlane/styles.ts b/vortex-web/src/components/swimlane/styles.ts deleted file mode 100644 index 0c82f2cbbf1..00000000000 --- a/vortex-web/src/components/swimlane/styles.ts +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -// Re-export constants from utils for backward compat, plus any rendering-only constants. -export { - ENCODING_STYLES, - getEncodingStyle, - DTYPE_COLORS, - DTYPE_CATEGORIES, - ROW_HEIGHT, - MIN_LABEL_WIDTH, - GROUP_SIZE, -} from './utils'; - -export const TREE_WIDTH = 260; -export const DEFAULT_SWIMLANE_MIN_WIDTH = 800; -export const DEFAULT_HEIGHT = 360; diff --git a/vortex-web/src/components/swimlane/types.ts b/vortex-web/src/components/swimlane/types.ts index 28e2a8f5ff3..e8bc1cf9c01 100644 --- a/vortex-web/src/components/swimlane/types.ts +++ b/vortex-web/src/components/swimlane/types.ts @@ -90,9 +90,11 @@ export interface FlattenedRow { // Retained from original types export type DtypeCategory = - 'bool' | 'int' | 'float' | 'utf8' | 'datetime' | 'struct' | 'list' | 'other'; - -export interface Split { - id: string; - rowRange: [number, number]; -} + | 'bool' + | 'int' + | 'float' + | 'utf8' + | 'datetime' + | 'struct' + | 'list' + | 'other'; diff --git a/vortex-web/src/components/swimlane/utils.ts b/vortex-web/src/components/swimlane/utils.ts index c640bc27491..9d7679cec7c 100644 --- a/vortex-web/src/components/swimlane/utils.ts +++ b/vortex-web/src/components/swimlane/utils.ts @@ -4,7 +4,6 @@ import type { LayoutTreeNode, ArrayEncodingNode, - Split, DtypeCategory, FlattenedRow, DisplayKind, @@ -12,83 +11,55 @@ import type { PhysicalSegment, } from './types'; -// Encoding styles — keyed by encoding string, with fallback for unknown encodings -export const ENCODING_STYLES: Record = { - 'vortex.struct': { color: '#5971FD', label: 'struct' }, - 'vortex.chunked': { color: '#CEE562', label: 'chunked' }, - 'vortex.flat': { color: '#2CB9D1', label: 'flat' }, - 'vortex.dict': { color: '#EEB3E1', label: 'dict' }, - 'vortex.zonemap': { color: '#FB863D', label: 'zonemap' }, - 'vortex.fsst': { color: '#EEB3E1', label: 'fsst' }, - 'vortex.roaring_bool': { color: '#EEB3E1', label: 'roaring' }, - 'vortex.roaring_int': { color: '#2CB9D1', label: 'roaring' }, - 'vortex.alp': { color: '#FB863D', label: 'alp' }, - 'vortex.alp_rd': { color: '#FB863D', label: 'alp-rd' }, - 'vortex.for': { color: '#CEE562', label: 'for' }, - 'vortex.bitpacked': { color: '#CEE562', label: 'bitpacked' }, - 'vortex.runend': { color: '#CEE562', label: 'run-end' }, - 'vortex.runend_bool': { color: '#CEE562', label: 'run-end' }, - 'vortex.zigzag': { color: '#2CB9D1', label: 'zigzag' }, - 'vortex.constant': { color: '#8F8F8F', label: 'const' }, - 'vortex.sparse': { color: '#8F8F8F', label: 'sparse' }, +// Encoding labels, keyed by encoding id, with a fallback for unknown encodings. Encodings take no +// colour: in the design system a hue names a dtype family, so an encoding is named in text. +export const ENCODING_STYLES: Record = { + 'vortex.struct': { label: 'struct' }, + 'vortex.chunked': { label: 'chunked' }, + 'vortex.flat': { label: 'flat' }, + 'vortex.dict': { label: 'dict' }, + 'vortex.zonemap': { label: 'zonemap' }, + 'vortex.fsst': { label: 'fsst' }, + 'vortex.roaring_bool': { label: 'roaring' }, + 'vortex.roaring_int': { label: 'roaring' }, + 'vortex.alp': { label: 'alp' }, + 'vortex.alp_rd': { label: 'alp-rd' }, + 'vortex.for': { label: 'for' }, + 'vortex.bitpacked': { label: 'bitpacked' }, + 'vortex.runend': { label: 'run-end' }, + 'vortex.runend_bool': { label: 'run-end' }, + 'vortex.zigzag': { label: 'zigzag' }, + 'vortex.constant': { label: 'const' }, + 'vortex.sparse': { label: 'sparse' }, }; -const DEFAULT_ENCODING_STYLE = { color: '#8F8F8F', label: 'unknown' }; - -export function getEncodingStyle(encoding: string): { color: string; label: string } { - return ( - ENCODING_STYLES[encoding] ?? { - ...DEFAULT_ENCODING_STYLE, - label: encoding.split('.').pop() ?? encoding, - } - ); +export function getEncodingStyle(encoding: string): { label: string } { + return ENCODING_STYLES[encoding] ?? { label: encoding.split('.').pop() ?? encoding }; } -// Dtype colors (for flat chunk bars in swimlane) -export const DTYPE_COLORS: Record = { - bool: '#D97BC6', - int: '#2CB9D1', - float: '#FB863D', - utf8: '#5971FD', - datetime: '#8BB536', - list: '#A78BFA', - struct: '#999999', - other: '#777777', -}; +/** The design system's class that sets `--vx-dt` to a dtype's colour. */ +export function dtypeClass(dtype?: string): string { + return `vx-dt-${getDtypeCategory(dtype)}`; +} -export const DTYPE_CATEGORIES: DtypeCategory[] = [ - 'bool', - 'int', - 'float', - 'utf8', - 'datetime', - 'list', - 'struct', - 'other', -]; - -export const ROW_HEIGHT = 26; -export const MIN_LABEL_WIDTH = 36; +export const ROW_HEIGHT = 28; export const GROUP_SIZE = 10; -/** Minimum rendered width (px) of a swimlane segment so tiny segments stay visible. */ -export const MIN_CELL_WIDTH = 3; /** - * Determine dtype category from dtype string + * The family of a dtype as Vortex prints it ("i64", "utf8?", "list(f32)", "{a=i64}"). The same + * rules as the design system's `dtypeFamily` (components/lib/dtype.js). */ export function getDtypeCategory(dtype?: string): DtypeCategory { if (!dtype) return 'other'; - const d = dtype.toLowerCase(); + const d = dtype.trim().toLowerCase().replace(/\?$/, ''); + // A container first: its field names and element types must not pick its family. + if (d.startsWith('{') || d.startsWith('struct')) return 'struct'; + if (/^(fixed_size_)?list\b/.test(d)) return 'list'; if (d === 'bool' || d === 'boolean') return 'bool'; - // Struct/list checks first — composite dtypes contain field names that would false-match others. - if (d.startsWith('{') || d === 'struct') return 'struct'; - if (d.includes('list') || d.includes('array')) return 'list'; - if (d.includes('utf8') || d.includes('string') || d.includes('binary')) return 'utf8'; - if (d.includes('timestamp') || d.includes('date') || d.includes('time')) return 'datetime'; - if (d.includes('int') || d.includes('uint') || d.startsWith('i') || d.startsWith('u')) - return 'int'; - if (d.includes('float') || d.includes('double') || d.includes('decimal') || d.startsWith('f')) - return 'float'; + if (/^(utf8|string|binary)\b/.test(d)) return 'utf8'; + if (/timestamp|^date|^time/.test(d)) return 'datetime'; + if (/^(u?int\d*|[iu]\d+)$/.test(d)) return 'int'; + if (/^(f\d+|float\d*|double|decimal)\b/.test(d)) return 'float'; return 'other'; } @@ -99,45 +70,6 @@ export function rangesOverlap(a: [number, number], b: [number, number]): boolean return a[0] < b[1] && b[0] < a[1]; } -/** - * Collect all row boundaries from a LayoutTreeNode tree - */ -export function collectBoundaries(node: LayoutTreeNode, set: Set = new Set()): Set { - set.add(node.rowOffset); - set.add(node.rowOffset + node.rowCount); - - for (const child of node.children) { - collectBoundaries(child, set); - } - - return set; -} - -/** - * Create splits from layout boundaries - */ -export function createSplits(layout: LayoutTreeNode): Split[] { - const boundaries = Array.from(collectBoundaries(layout)).sort((a, b) => a - b); - return boundaries.slice(0, -1).map((start, i) => ({ - id: `s${i}`, - rowRange: [start, boundaries[i + 1]] as [number, number], - })); -} - -/** - * Get the combined row range of selected splits - */ -export function getSelectedRowRange( - splits: Split[], - selectedSplits: Set, -): [number, number] | null { - if (selectedSplits.size === 0) return null; - const selected = splits.filter((s) => selectedSplits.has(s.id)); - const min = Math.min(...selected.map((s) => s.rowRange[0])); - const max = Math.max(...selected.map((s) => s.rowRange[1])); - return [min, max]; -} - /** * Get the display name for a layout tree node based on its child type */ @@ -157,32 +89,6 @@ export function getNodeDisplayName(node: LayoutTreeNode): string { } } -/** - * Get a badge label for schema mode - */ -export function getSchemaLabel(node: LayoutTreeNode): string { - return node.dtype; -} - -/** - * Get a badge label for layout mode - */ -export function getLayoutLabel(node: LayoutTreeNode): string { - const ct = node.childType; - switch (ct.kind) { - case 'root': - return getEncodingStyle(node.encoding).label; - case 'field': - return `[field] ${getEncodingStyle(node.encoding).label}`; - case 'chunk': - return `[chunk ${ct.chunkIndex}]`; - case 'transparent': - return `[transparent: ${ct.name}]`; - case 'auxiliary': - return `[aux: ${ct.name}]`; - } -} - /** * Check if a node has expandable children */ @@ -231,8 +137,8 @@ export function groupChildren( const groupNodes = children.slice(i, Math.min(i + GROUP_SIZE, children.length)); const startIdx = i; const endIdx = Math.min(i + GROUP_SIZE - 1, children.length - 1); - const firstNode = groupNodes[0]; - const lastNode = groupNodes[groupNodes.length - 1]; + const firstNode = groupNodes[0]!; + const lastNode = groupNodes[groupNodes.length - 1]!; groups.push({ id: `${parentId}_group_${startIdx}_${endIdx}`, @@ -408,37 +314,6 @@ export function findPathToNode(root: LayoutTreeNode, id: string): LayoutTreeNode return []; } -/** - * Collect all node IDs in a subtree - */ -export function collectSubtreeIds(node: LayoutTreeNode): Set { - const ids = new Set(); - function walk(n: LayoutTreeNode) { - ids.add(n.id); - for (const child of n.children) walk(child); - } - walk(node); - return ids; -} - -/** - * Collect the leaf descendants of a node (nodes with no children), in row order. - * Used to roll up a parent's data into a single swimlane bar so that container - * layouts (structs, chunked, zone maps) are not rendered as empty outlines. - */ -export function collectLeafDescendants(node: LayoutTreeNode): LayoutTreeNode[] { - const leaves: LayoutTreeNode[] = []; - function walk(n: LayoutTreeNode) { - if (n.children.length === 0) { - leaves.push(n); - return; - } - for (const child of n.children) walk(child); - } - for (const child of node.children) walk(child); - return leaves; -} - /** * Build a lookup from segment index to its physical byte placement and the dtype * of the layout node that owns it. Lets the swimlane plot bars by physical file @@ -548,11 +423,12 @@ export function shortEncoding(encoding: string): string { /** * Format bytes to human readable string */ +/** Bytes in decimal units (1 KB is 1,000 bytes), as Writing sets them and bench prints them. */ export function formatBytes(bytes: number): string { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; - return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; + if (bytes < 1e3) return `${bytes} B`; + if (bytes < 1e6) return `${(bytes / 1e3).toFixed(1)} KB`; + if (bytes < 1e9) return `${(bytes / 1e6).toFixed(1)} MB`; + return `${(bytes / 1e9).toFixed(1)} GB`; } /** @@ -566,10 +442,12 @@ export function formatRowRange(range: [number, number]): string { /** * Format row count to compact string (e.g., "27.1k") */ +/** A short count for a meta line ("100k", "1.5M"). Tables and facts print the full count. */ export function formatRowCount(n: number): string { + const short = (v: number) => (v >= 100 ? v.toFixed(0) : v.toFixed(1).replace(/\.0$/, '')); if (n < 1000) return String(n); - if (n < 1000000) return `${(n / 1000).toFixed(1)}k`; - return `${(n / 1000000).toFixed(1)}M`; + if (n < 1e6) return `${short(n / 1e3)}k`; + return `${short(n / 1e6)}M`; } /** diff --git a/vortex-web/src/contexts/ThemeContext.tsx b/vortex-web/src/contexts/ThemeContext.tsx index 722ea60b5c9..97b14f990ef 100644 --- a/vortex-web/src/contexts/ThemeContext.tsx +++ b/vortex-web/src/contexts/ThemeContext.tsx @@ -11,23 +11,25 @@ export interface ThemeContextValue { setTheme: (theme: ThemeChoice) => void; } -const STORAGE_KEY = 'vortex-theme'; - +// The system's ThemeSwitch key. The Explorer stored 'vortex-theme' before; theme.js moves it. +const STORAGE_KEY = 'vx-theme'; + +/** + * The design system's tokens read the choice from `data-theme` on the root: `dark`, `light`, or + * `system`, which follows the operating system through prefers-color-scheme. `public/theme.js` + * sets it before the first paint; this keeps it in step with the switch. + */ function applyTheme(choice: ThemeChoice) { - const root = document.documentElement; - root.classList.remove('light', 'dark'); - - if (choice === 'system') { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - root.classList.add(prefersDark ? 'dark' : 'light'); - } else { - root.classList.add(choice); - } + document.documentElement.dataset.theme = choice; } function getStoredTheme(): ThemeChoice { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored === 'light' || stored === 'dark' || stored === 'system') return stored; + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === 'light' || stored === 'dark' || stored === 'system') return stored; + } catch { + // Storage can be blocked; fall back to the default. + } return 'dark'; } @@ -35,22 +37,15 @@ export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setThemeState] = useState(getStoredTheme); const setTheme = useCallback((choice: ThemeChoice) => { - localStorage.setItem(STORAGE_KEY, choice); + try { + localStorage.setItem(STORAGE_KEY, choice); + } catch { + // The choice then lasts for this page only. + } setThemeState(choice); - applyTheme(choice); }, []); - // Apply on mount and listen for system preference changes. - useEffect(() => { - applyTheme(theme); - - if (theme !== 'system') return; - - const mq = window.matchMedia('(prefers-color-scheme: dark)'); - const handler = () => applyTheme('system'); - mq.addEventListener('change', handler); - return () => mq.removeEventListener('change', handler); - }, [theme]); + useEffect(() => applyTheme(theme), [theme]); return {children}; } diff --git a/vortex-web/src/index.css b/vortex-web/src/index.css index 432e388dca6..c0c8dda94ee 100644 --- a/vortex-web/src/index.css +++ b/vortex-web/src/index.css @@ -1,91 +1,10 @@ /* SPDX-License-Identifier: Apache-2.0 */ /* SPDX-FileCopyrightText: Copyright the Vortex contributors */ -@import 'tailwindcss'; - -@custom-variant dark (&:where(.dark, .dark *)); - -@theme inline { - --color-grey: #d4d4d8; - - /* Surface palette */ - --color-vortex-black: #1a1a1e; - --color-vortex-white: #fafafa; - --color-vortex-grey: #e4e4e7; - --color-vortex-grey-dark: #71717a; - --color-vortex-grey-light: #d4d4d8; - --color-vortex-grey-lightest: #f4f4f5; - - /* Foreground — separate from surfaces so text contrast is tunable */ - --color-vortex-fg: #e4e4e8; - --color-vortex-fg-light: #18181b; - - /* Accent colors */ - --color-vortex-green: #cee562; - --color-vortex-washed-green: #f1f8d0; - --color-vortex-light-blue: #2cb9d1; - --color-vortex-washed-light-blue: #d5f1f6; - --color-vortex-washed-lighter-blue: #eaf8fa; - --color-vortex-blue: #5971fd; - --color-vortex-pink: #eeb3e1; - --color-vortex-orange: #fb863d; - --color-vortex-red: #dc2626; - - --font-sans: 'Geist', sans-serif; - --font-mono: 'Geist Mono', monospace; - --font-funnel: 'Funnel Display', sans-serif; -} - -html { - scrollbar-width: thin; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.light, -.light * { - scrollbar-color: var(--color-vortex-grey-light) var(--color-vortex-grey-lightest); -} - -.dark, -.dark * { - scrollbar-color: #333338 var(--color-vortex-black); -} - -body { - font-family: theme('fontFamily.sans'); - text-rendering: optimizeLegibility; -} - -/* Default app is dark themed */ -:root:not(.light) { - --array-node-bg: rgba(255, 255, 255, 0.03); -} -:root:not(.light) body { - background: var(--color-vortex-black); -} - -.light { - --array-node-bg: rgba(0, 0, 0, 0.025); -} -.light body { - background: var(--color-vortex-white); -} - -@layer utilities { - .dashed-bottom { - @apply relative after:absolute after:left-0 after:right-0 after:bottom-0 after:h-px after:bg-[length:16px_1px] after:bg-repeat-x after:bg-[linear-gradient(to_right,currentColor_12px,transparent_4px)] after:opacity-20; - } - - .dashed-top { - @apply relative before:absolute before:left-0 before:right-0 before:top-0 before:h-px before:bg-[length:16px_1px] before:bg-repeat-x before:bg-[linear-gradient(to_right,currentColor_12px,transparent_4px)] before:opacity-20; - } - - .dashed-right { - @apply relative after:absolute after:top-0 after:bottom-0 after:right-0 after:w-px after:bg-[length:1px_16px] after:bg-repeat-y after:bg-[linear-gradient(to_bottom,currentColor_12px,transparent_4px)] after:opacity-20; - } - - .dashed-left { - @apply relative before:absolute before:top-0 before:bottom-0 before:left-0 before:w-px before:bg-[length:1px_16px] before:bg-repeat-y before:bg-[linear-gradient(to_bottom,currentColor_12px,transparent_4px)] before:opacity-20; - } -} +/* The Vortex design system, then the Explorer's own rules. vortex-*.css are copies made by the + system's tools/site-styles.mjs (sections base, site, tables, app, and focus): change the system, + then copy again. */ +@import './styles/vortex-tokens.css'; +@import './styles/vortex-base.css'; +@import './styles/vortex.css'; +@import './styles/explorer.css'; diff --git a/vortex-web/src/mocks/layouts.ts b/vortex-web/src/mocks/layouts.ts index c181de20275..b685a9e6f48 100644 --- a/vortex-web/src/mocks/layouts.ts +++ b/vortex-web/src/mocks/layouts.ts @@ -126,7 +126,7 @@ export function wideMock(): LayoutTreeNode { const dtypes = ['i32', 'i64', 'f32', 'f64', 'utf8', 'bool', 'u16', 'u32']; const fields: LayoutTreeNode[] = Array.from({ length: 200 }, (_, i) => { - const dtype = dtypes[i % dtypes.length]; + const dtype = dtypes[i % dtypes.length]!; return makeFlat({ id: `root.col_${i}`, childType: { kind: 'field', fieldName: `col_${i}` }, diff --git a/vortex-web/src/styles/explorer.css b/vortex-web/src/styles/explorer.css new file mode 100644 index 00000000000..045034481a2 --- /dev/null +++ b/vortex-web/src/styles/explorer.css @@ -0,0 +1,779 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* SPDX-FileCopyrightText: Copyright the Vortex contributors */ + +/* The Explorer's own rules, on top of the Vortex design system (vortex-tokens.css, vortex-base.css + and vortex.css, which are copies: change the system, not them). A value is a token wherever + the system has one. The exceptions are fixed widths, SVG stroke widths, and the run frame's + corner. The classes start with x-. */ + +/* ------------------------------------------------------------------ the frame + One screen: the header, the work area, and the status line. The panes scroll, the page does not. + A tool wants its room for data, so the page's side gutter steps down from 48px to 24px, and to + the token's 16px on a phone. */ +.x-app { + --page-pad: var(--space-24); + display: flex; + flex-direction: column; + height: 100dvh; + overflow: hidden; + position: relative; +} +.x-app > .vx-header { + flex: none; +} +/* The tools and the end group sit together at the right. */ +.x-app .vx-header-tools + .vx-header-end { + margin-left: 0; +} + +.x-file { + display: flex; + align-items: baseline; + gap: var(--space-12); + min-width: 0; +} +.x-file-name { + color: var(--ink); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.x-file-meta { + white-space: nowrap; +} +.x-icon-btn { + width: var(--control-md); + padding: 0; + justify-content: center; +} + +.x-work { + flex: 1 1 auto; + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 300px) minmax(0, 1fr); + gap: var(--space-16); + padding: var(--space-16) var(--page-pad); +} +.x-work.is-wide { + grid-template-columns: minmax(0, 1fr); +} + +/* The view and the summary sit over the preview. The handle between them is the row gap, so the + preview's height changes by dragging it. */ +.x-main { + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 248px); + grid-template-rows: minmax(0, 1fr) var(--space-16) var(--x-preview-height, 240px); + column-gap: var(--space-16); +} +.x-view { + grid-column: 1; + grid-row: 1; +} +.x-side { + grid-column: 2; + grid-row: 1; +} +.x-handle { + grid-column: 1 / -1; + grid-row: 2; + position: relative; + cursor: row-resize; + touch-action: none; +} +.x-handle::after { + content: ''; + position: absolute; + inset: calc(50% - 1px) calc(50% - var(--space-24)); + border-radius: var(--radius-full); + background: var(--edge); + transition: background-color var(--vx-motion); +} +.x-handle:hover::after, +.x-handle:focus-visible::after { + background: var(--ink-muted); +} +.x-handle:focus-visible { + outline-offset: -2px; +} +.x-preview { + grid-column: 1 / -1; + grid-row: 3; +} + +/* A pane's body with no table or tree in it gets the card's padding. */ +.x-pad { + padding: var(--space-16); +} +.x-empty { + display: grid; + place-items: center; + height: 100%; + padding: var(--space-16); + text-align: center; +} +.x-error { + color: var(--error-ink); +} +.x-file-error { + display: grid; + gap: var(--space-4); + justify-items: center; + text-align: center; +} + +/* The status line runs the width of the window, at the page's edge. */ +.x-status { + flex: none; + padding-inline: var(--page-pad); + background: var(--ground); +} +.x-status .x-push { + margin-left: auto; +} + +/* ------------------------------------------------------------------ the tree + The search field (vx-field is-sm) fills the head beside the switch. */ +.x-tree-pane .vx-pane-head .vx-field { + flex: 1 1 120px; + min-width: 0; +} +.x-tree-name { + display: flex; + align-items: center; + min-width: 0; +} +.x-tree-label { + flex: none; + white-space: nowrap; +} +.x-tree-name .vx-tree-type { + min-width: 0; + margin-left: var(--space-8); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +/* A node the layout groups (a run of chunks) or hides (a transparent layout) is quieter. */ +.vx-tree-node.is-group, +.vx-tree-node.is-hidden { + color: var(--ink-muted); +} +.vx-tree-node.is-hidden { + cursor: default; +} +/* An array encoding node, under a flat layout, takes the wash so it reads as inside the segment. */ +.vx-tree-node.is-array { + background: var(--wash); +} +/* Hover in another view marks the node and, more faintly, its parents. */ +.vx-tree-node.is-hovered { + background: var(--raised); +} +.vx-tree-node.is-hovered-parent { + background: var(--wash); +} + +/* ------------------------------------------------------------------ the detail view */ +.x-view-head { + flex: none; + display: flex; + align-items: center; + gap: var(--space-12); + border-bottom: 1px solid var(--line); + padding-right: var(--space-16); +} +.x-view-head > .vx-tabs-list { + flex: none; + border-bottom: 0; +} +.x-crumbs { + display: flex; + align-items: center; + gap: var(--space-4); + min-width: 0; + margin-left: auto; + overflow: hidden; + white-space: nowrap; +} +.x-crumbs ol { + display: flex; + align-items: center; + gap: var(--space-4); + min-width: 0; +} +.x-crumbs li { + display: flex; + align-items: center; + gap: var(--space-4); + min-width: 0; +} +.x-crumbs li + li::before { + content: '/' / ''; + color: var(--ink-muted); +} +.x-crumbs li.is-array::before { + content: '›' / ''; +} +.x-crumbs button { + all: unset; + cursor: pointer; + color: var(--ink-muted); + overflow: hidden; + text-overflow: ellipsis; + transition: color var(--vx-motion); +} +.x-crumbs button:hover { + color: var(--ink); +} +.x-crumbs [aria-current] { + color: var(--ink); + overflow: hidden; + text-overflow: ellipsis; +} +.x-crumbs.is-preview { + opacity: 0.6; +} +.x-dtype { + flex: none; + display: inline-flex; + align-items: center; + gap: var(--space-4); +} +.x-dtype::before { + content: ''; + width: 8px; + height: 8px; + background: var(--vx-dt, var(--chart-muted)); +} + +.x-side .vx-facts { + grid-template-columns: minmax(0, 1fr); +} +.x-side-title { + overflow-wrap: anywhere; +} + +/* The encoding tree, as text. */ +.x-encoding { + padding: var(--space-16); + font: 400 12.5px / 1.6 var(--font-mono); + color: var(--ink-body); + white-space: pre; + overflow: auto; +} + +/* ------------------------------------------------------------------ the buffers */ +.x-buffers { + display: grid; + grid-template-columns: minmax(0, 176px) minmax(0, 1fr); + height: 100%; +} +.x-buffer-list { + min-width: 0; + overflow-y: auto; + border-right: 1px solid var(--line); +} +.x-buffer-list button { + all: unset; + box-sizing: border-box; + display: grid; + gap: var(--space-4); + width: 100%; + padding: var(--space-8) var(--space-16); + border-bottom: 1px solid var(--line); + cursor: pointer; + transition: background-color var(--vx-motion); +} +.x-buffer-list button:hover { + background: var(--wash); +} +.x-buffer-list button[aria-pressed='true'] { + background: var(--accent-soft); + box-shadow: inset 2px 0 0 var(--accent); +} +.x-buffer-list button:focus-visible { + outline-offset: -2px; +} +.x-buffer-name { + color: var(--ink); + font: 400 12.5px / 1.3 var(--font-mono); +} +.x-hex { + overflow: auto; + padding: var(--space-12) var(--space-16); + font: 400 12px / 20px var(--font-mono); + font-variant-numeric: tabular-nums; + color: var(--ink-body); +} +.x-hex-row { + display: flex; + gap: var(--space-16); + white-space: pre; +} +.x-hex-offset { + flex: none; + color: var(--ink-muted); + user-select: none; +} +.x-hex-bytes, +.x-hex-text { + flex: none; +} +.x-hex-byte { + display: inline-block; + width: 2.5ch; + text-align: center; +} +.x-hex-byte.is-gap { + margin-left: 1ch; +} +.x-hex-text .is-dot { + color: var(--ink-muted); +} +.x-hex [data-at].is-hovered { + background: var(--accent-soft); + color: var(--ink); +} + +/* ------------------------------------------------------------------ the data table + The preview and the segment list: a vx-table with sticky heads, virtualised rows, and a short + summary of each column in its head. */ +.x-table-scroll { + height: 100%; + overflow: auto; + scrollbar-width: thin; + scrollbar-color: var(--edge) transparent; +} +.x-table-scroll .vx-table.is-data { + width: max-content; + min-width: 100%; +} +.x-table-scroll .vx-table th { + white-space: nowrap; +} +.x-table-scroll .vx-table th button { + all: unset; + display: inline-flex; + align-items: center; + gap: var(--space-8); + cursor: pointer; +} +.x-table-scroll .vx-table th[aria-sort] button { + color: var(--ink); +} +.x-table-scroll .vx-table.is-data th:first-child, +.x-table-scroll .vx-table.is-data td:first-child { + padding-left: var(--space-16); +} +.x-table-scroll .vx-table.is-data td { + max-width: 320px; + overflow: hidden; + text-overflow: ellipsis; +} +.x-table-scroll .vx-table tr.is-link { + cursor: pointer; +} +.x-null, +.x-row-num { + color: var(--ink-muted); +} +.x-col-summary { + color: var(--ink-muted); + font-weight: 400; +} +.x-spark { + display: block; + flex: none; +} +.x-spark rect { + fill: var(--chart-muted); +} +.x-spark line { + stroke: var(--chart-muted); +} +.x-share { + display: inline-flex; + width: 32px; + height: 6px; + background: var(--chart-track); +} +.x-share > span { + background: var(--chart-muted); +} +.x-tip.x-col-tip { + position: absolute; + pointer-events: none; + font-weight: 400; + z-index: 2; + top: 100%; + left: 0; + min-width: 160px; + margin-top: var(--space-4); +} +.x-path-link { + all: unset; + cursor: pointer; + color: var(--ink-muted); + transition: color var(--vx-motion); +} +.x-path-link:hover { + color: var(--ink); +} +.x-path-sep { + color: var(--ink-muted); + padding: 0 2px; +} + +/* ------------------------------------------------------------------ tooltips + A card that follows the pointer, with a key and value list. */ +.x-tip { + position: fixed; + z-index: 1000; + pointer-events: none; + max-width: 260px; + background: var(--raised); + border: 1px solid var(--edge); + border-radius: var(--radius-card); +} +.x-tip-head { + display: flex; + align-items: center; + gap: var(--space-8); + padding: var(--space-8) var(--space-12) 0; + color: var(--ink); +} +.x-tip .vx-facts { + gap: var(--space-4) var(--space-12); + padding: var(--space-8) var(--space-12) var(--space-12); +} +.x-tip .vx-facts > div { + grid-template-columns: auto 1fr; + align-items: baseline; + gap: var(--space-8); +} +.x-tip .vx-facts dd { + font-size: 12px; +} + +/* ------------------------------------------------------------------ the treemap + A leaf takes its dtype's fill (--vx-dt-fill: solid in Dark, a tint with a hued edge in Light), + and a container is only an outline round its leaves. The selected block takes the accent ring + (Blue marks a selection), and a hovered one an ink stroke. The SVG reads its colours from the + tokens, so it follows the theme. */ +.x-treemap { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} +.x-treemap svg { + display: block; + cursor: pointer; + user-select: none; +} +.x-treemap rect { + fill: none; + stroke: var(--edge); + stroke-width: 1; +} +.x-treemap .is-leaf rect { + fill: var(--vx-dt-fill, var(--raised)); + stroke: var(--vx-dt-edge, var(--surface)); +} +.x-treemap .is-hovered rect { + stroke: var(--ink); + stroke-width: 1; +} +.x-treemap .is-selected rect { + stroke: var(--accent); + stroke-width: 2; +} +/* The selected node is the drill root, which draws no block: the frame takes the ring. */ +.x-treemap.is-selected::after { + content: ''; + position: absolute; + inset: 0; + border: 2px solid var(--accent); + pointer-events: none; +} +.x-treemap text { + font: 400 11px var(--font-mono); + fill: var(--ink); +} +.x-treemap .is-container text { + font-weight: 600; +} +.x-treemap .is-leaf text { + fill: var(--vx-dt-ink, var(--ink)); +} +.x-treemap text.is-size { + fill: var(--ink-body); +} +.x-treemap .is-leaf text.is-size { + fill: var(--vx-dt-ink, var(--ink)); + fill-opacity: 0.85; +} +.x-treemap-up { + position: absolute; + z-index: 1; + top: var(--space-8); + left: var(--space-8); + background: var(--surface); +} + +/* ------------------------------------------------------------------ the swimlane + One row per column: its name, then its segments at their place in the file. */ +.x-lanes { + display: flex; + flex-direction: column; + height: 100%; +} +.x-lanes-rows { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + scrollbar-gutter: stable; + padding: var(--space-8) 0; +} +.x-lane { + display: grid; + grid-template-columns: var(--x-lane-label, 180px) minmax(0, 1fr); + min-height: var(--control-sm); + transition: background-color var(--vx-motion); +} +.x-lane:hover { + background: var(--wash); +} +.x-lane.is-selected { + background: var(--accent-soft); +} +.x-lane.is-hovered { + background: var(--wash); +} +.x-lane-name { + display: flex; + align-items: center; + min-width: 0; + padding-left: calc(var(--space-12) + var(--vx-depth, 0) * var(--space-16)); + padding-right: var(--space-8); +} +.x-lane-select { + all: unset; + box-sizing: border-box; + display: flex; + align-items: center; + gap: var(--space-8); + min-width: 0; + height: 100%; + font: 400 12.5px / 1 var(--font-mono); + color: var(--ink-body); + cursor: pointer; + white-space: nowrap; +} +.x-lane-select:focus-visible { + outline-offset: -2px; +} +.x-lane-select > .vx-tree-dot { + flex: none; +} +.x-lane-select > span:last-child { + overflow: hidden; + text-overflow: ellipsis; +} +.x-lane-select:hover, +.x-lane.is-selected .x-lane-select { + color: var(--ink); +} +/* The chevron is the tree's (vx-tree-toggle), which turns under aria-expanded. */ +.x-lane-toggle { + all: unset; + flex: none; + display: grid; + place-items: center; + cursor: pointer; +} +.x-lane-toggle .vx-tree-toggle { + vertical-align: 0; +} +.x-lane-toggle.is-leaf { + visibility: hidden; +} +.x-lane-bar { + position: relative; + min-width: 0; +} +/* A segment: a thin mark, so it takes its dtype's full hue in both themes (the treemap's tint is + for areas). A run of segments in one column sits in a frame, which takes the accent when the + column is selected. */ +.x-seg { + position: absolute; + top: 6px; + bottom: 6px; + min-width: 3px; + background: var(--vx-dt, var(--chart-muted)); +} +.x-run { + all: unset; + box-sizing: border-box; + position: absolute; + top: 4px; + bottom: 4px; + min-width: 3px; + border: 1px solid var(--edge); + border-radius: 2px; + cursor: pointer; +} +.x-lane.is-selected .x-run { + border-color: var(--accent); + box-shadow: inset 0 0 0 1px var(--accent); +} +.x-axis { + flex: none; + display: grid; + grid-template-columns: var(--x-lane-label, 180px) minmax(0, 1fr); + border-top: 1px solid var(--line); + scrollbar-gutter: stable; + overflow: hidden; +} +.x-axis-ticks { + position: relative; + height: var(--control-sm); +} +.x-axis-ticks > span { + position: absolute; + top: var(--space-8); + translate: -50% 0; + white-space: nowrap; +} +.x-axis-ticks > span:first-child { + translate: 0 0; +} +/* The file's size ends the axis, set in from the card's edge. */ +.x-axis-ticks > span:last-child { + translate: calc(-100% - var(--space-8)) 0; +} + +/* ------------------------------------------------------------------ dropping a file */ +.x-drop-veil { + position: absolute; + inset: 0; + z-index: 1001; + display: grid; + place-items: center; + background: color-mix(in srgb, var(--ground) 80%, transparent); + border: 2px dashed var(--edge); + pointer-events: none; +} + +/* ------------------------------------------------------------------ the open screen */ +.x-open { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + display: grid; + align-content: center; + justify-items: center; + padding: var(--space-48) var(--page-pad); +} +.x-open-body { + display: grid; + gap: var(--space-32); + width: 100%; + max-width: 640px; +} +.x-open-head { + display: grid; + gap: var(--space-12); +} +.x-open-head p { + color: var(--ink-body); +} +.x-drop { + display: grid; + justify-items: center; + gap: var(--space-16); + padding: var(--space-48) var(--space-24); + text-align: center; + transition: border-color var(--vx-motion); +} +.x-drop:hover, +.x-drop.is-over { + border-color: var(--ink); +} +.x-drop .vx-mono { + color: var(--ink); +} +.x-url { + display: grid; + gap: var(--space-8); +} +.x-url .vx-field-row { + max-width: none; +} + +/* ------------------------------------------------------------------ narrow windows + Below 1100px the summary moves under the view. Below 760px the panes stack and the page scrolls: + the Explorer is built for a desktop window, and a phone gets a readable column. */ +@media (max-width: 1100px) { + .x-file-meta { + display: none; + } + .x-main { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) auto var(--space-16) var(--x-preview-height, 240px); + row-gap: 0; + } + .x-side { + grid-column: 1; + grid-row: 2; + margin-top: var(--space-16); + max-height: 200px; + } + .x-side .vx-facts { + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + } + .x-handle { + grid-row: 3; + } + .x-preview { + grid-row: 4; + } +} +@media (max-width: 760px) { + .x-app { + height: auto; + overflow: visible; + } + .x-work { + grid-template-columns: minmax(0, 1fr); + } + .x-tree-pane { + max-height: 360px; + } + .x-view { + min-height: 420px; + } + .x-file { + display: none; + } + .x-status .x-push { + margin-left: 0; + } +} +/* Below 640px the view switch leaves the row, and its twin rows in the Menu's panel take over. The + current view reads as the current page does. */ +@media (max-width: 640px) { + .x-app { + --page-pad: var(--space-16); + } + .x-app .vx-header-tools { + display: none; + } +} +.vx-header-menu-panel .vx-nav-link[aria-pressed='true'] { + color: var(--ink); +} diff --git a/vortex-web/src/styles/vortex-base.css b/vortex-web/src/styles/vortex-base.css new file mode 100644 index 00000000000..26adcb3bae7 --- /dev/null +++ b/vortex-web/src/styles/vortex-base.css @@ -0,0 +1,155 @@ +/* Vortex design system base, copied by tools/site-styles.mjs. + Edit the system, then run tools/site-styles.mjs; do not edit here. + Made by: node tools/site-styles.mjs --sections base,site,tables,app,focus --base + sha256 7f7de2b97b16099662cddb9f80a900a2d8b4fdb967eef65bc47b8e773b540bf6 */ + +/* Vortex base: a small reset for a site drawn with the system, such as vortex.dev after Tailwind + left. Load it with the tokens and bundle.css. The components never depend on it: they draw the + same under this reset, another one or none. Every rule sits in the vx-base layer, so any rule + outside a layer, the system's and the site's, overrides it. The docs keep Sphinx's own styles + and do not load it. */ +@layer vx-base { + *, + ::before, + ::after, + ::backdrop { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + + /* The page: the theme's ground, body ink and Geist, with thin scrollbars in its greys. */ + html { + line-height: 1.5; + font-family: var(--font-sans); + -webkit-text-size-adjust: 100%; + tab-size: 4; + -webkit-tap-highlight-color: transparent; + scrollbar-width: thin; + scrollbar-color: var(--edge) var(--ground); + /* A stray wide element never scrolls the page sideways. clip, not hidden: hidden makes the + root a scroll container, and position: sticky stops working under it. */ + overflow-x: clip; + } + + body { + background: var(--ground); + color: var(--ink-body); + font-family: var(--font-sans); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* The root's clip, again on body: some engines scroll body, not the root. */ + overflow-x: clip; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + + a { + color: inherit; + text-decoration: inherit; + } + + b, + strong { + font-weight: bolder; + } + + code, + kbd, + samp, + pre { + font-family: var(--font-mono); + font-size: 1em; + } + + small { + font-size: 80%; + } + + sub, + sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + + ol, + ul, + menu { + list-style: none; + } + + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; + vertical-align: middle; + } + + img, + video { + max-width: 100%; + height: auto; + } + + button, + input, + select, + optgroup, + textarea { + font: inherit; + letter-spacing: inherit; + color: inherit; + background-color: transparent; + border-radius: 0; + opacity: 1; + } + + ::placeholder { + opacity: 1; + } + + textarea { + resize: vertical; + } + + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} diff --git a/vortex-web/src/styles/vortex-tokens.css b/vortex-web/src/styles/vortex-tokens.css new file mode 100644 index 00000000000..6f736e2a23e --- /dev/null +++ b/vortex-web/src/styles/vortex-tokens.css @@ -0,0 +1,207 @@ +/* Vortex design system tokens, copied by tools/site-styles.mjs. + Edit the system, then run tools/site-styles.mjs; do not edit here. + Made by: node tools/site-styles.mjs --sections base,site,tables,app,focus --base + sha256 a837268db37ce246591addb34264a88e9290b620a84c894fdf8b4b191280b76f */ +:root, [data-theme="dark"] { + --white: #ffffff; + --grey-50: #fcfcf9; + --grey-100: #ebebe8; + --grey-200: #dfdfdd; + --grey-300: #d4d4d2; + --grey-400: #bebebb; + --grey-450: #9c9c99; + --grey-500: #646462; + --grey-600: #3c3c3a; + --grey-700: #2b2b29; + --black: #171716; + --blue: #1d0fed; + --blue-bright: #2f63ff; + --blue-soft: #8fa9ff; + --purple: #8731dd; + --pink: #dd317e; + --orange: #f27332; + --red: #e43134; + --green: #17b65a; + --ground-dark: #0f0f0e; + --ground-light: #f5f5f2; + --ground: var(--ground-dark); + --surface: var(--black); + --raised: var(--grey-700); + --line: var(--grey-700); + --edge: var(--grey-600); + --wash: rgba(43, 43, 41, 0.5); + --ink: var(--white); + --ink-body: var(--grey-300); + --ink-muted: var(--grey-450); + --accent: var(--blue-bright); + --accent-ink: var(--blue-soft); + --accent-soft: rgba(47, 99, 255, 0.16); + --primary: var(--grey-50); + --on-primary: var(--black); + --on-accent: var(--white); + --focus-ring: var(--accent-ink); + --error-ink: #f26a6b; + --warn-ink: var(--orange); + --syntax-plain: var(--grey-300); + --syntax-keyword: var(--blue-soft); + --syntax-function: #c9a8f5; + --syntax-type: #6fcfd6; + --syntax-string: #f2a0c2; + --syntax-comment: var(--grey-450); + --chart-vortex: var(--accent); + --chart-muted: var(--grey-600); + --chart-parquet: #0d8a97; + --chart-lance: #976fa1; + --chart-arrow: #c45d68; + --chart-track: var(--grey-700); + --chart-2: #e7b969; + --chart-3: #bbdc7d; + --chart-4: #e672a3; + --chart-5: #60c1d7; + --chart-6: #c18ae2; + --chart-fill-mix: 100%; + --chart-edge-mix: 0%; + --chart-fill-ink: var(--black); + --status-ok: var(--green); + --status-warn: var(--orange); + --status-error: var(--red); + --status-idle: var(--grey-500); + color-scheme: dark; +} +[data-theme="light"] { + --ground: var(--ground-light); + --surface: var(--white); + --raised: var(--grey-100); + --line: var(--grey-200); + --edge: var(--grey-300); + --wash: rgba(235, 235, 232, 0.5); + --ink: var(--black); + --ink-body: var(--grey-600); + --ink-muted: var(--grey-500); + --accent: var(--blue); + --accent-ink: var(--blue); + --accent-soft: rgba(29, 15, 237, 0.1); + --primary: var(--black); + --on-primary: var(--white); + --on-accent: var(--white); + --focus-ring: var(--accent-ink); + --error-ink: #c42b2e; + --warn-ink: #ad4a00; + --syntax-plain: var(--grey-600); + --syntax-keyword: var(--blue); + --syntax-function: #6d24b5; + --syntax-type: #0a6b75; + --syntax-string: #a8205b; + --syntax-comment: var(--grey-500); + --chart-vortex: var(--accent); + --chart-muted: var(--grey-300); + --chart-track: var(--grey-100); + --chart-2: #b17000; + --chart-3: #5e8a21; + --chart-4: var(--pink); + --chart-5: #16808f; + --chart-6: var(--purple); + --chart-fill-mix: 40%; + --chart-edge-mix: 100%; + --chart-fill-ink: var(--ink); + --status-ok: var(--green); + --status-warn: var(--orange); + --status-error: var(--red); + --status-idle: var(--grey-400); + color-scheme: light; +} +@media (prefers-color-scheme: light) { + [data-theme="system"] { + --ground: var(--ground-light); + --surface: var(--white); + --raised: var(--grey-100); + --line: var(--grey-200); + --edge: var(--grey-300); + --wash: rgba(235, 235, 232, 0.5); + --ink: var(--black); + --ink-body: var(--grey-600); + --ink-muted: var(--grey-500); + --accent: var(--blue); + --accent-ink: var(--blue); + --accent-soft: rgba(29, 15, 237, 0.1); + --primary: var(--black); + --on-primary: var(--white); + --on-accent: var(--white); + --focus-ring: var(--accent-ink); + --error-ink: #c42b2e; + --warn-ink: #ad4a00; + --syntax-plain: var(--grey-600); + --syntax-keyword: var(--blue); + --syntax-function: #6d24b5; + --syntax-type: #0a6b75; + --syntax-string: #a8205b; + --syntax-comment: var(--grey-500); + --chart-vortex: var(--accent); + --chart-muted: var(--grey-300); + --chart-track: var(--grey-100); + --chart-2: #b17000; + --chart-3: #5e8a21; + --chart-4: var(--pink); + --chart-5: #16808f; + --chart-6: var(--purple); + --chart-fill-mix: 40%; + --chart-edge-mix: 100%; + --chart-fill-ink: var(--ink); + --status-ok: var(--green); + --status-warn: var(--orange); + --status-error: var(--red); + --status-idle: var(--grey-400); + color-scheme: light; + } +} +:root { + --space-4: 4px; + --space-8: 8px; + --space-12: 12px; + --space-16: 16px; + --space-18: 18px; + --space-20: 20px; + --space-24: 24px; + --space-32: 32px; + --space-48: 48px; + --space-64: 64px; + --space-80: 80px; + --space-120: 120px; + --space-152: 152px; + --radius-control: 4px; + --radius-card: 6px; + --radius-full: 999px; + --page-max: 1440px; + --content-max: 1344px; + --measure: 54ch; + --page-pad: 48px; + --gutter: 24px; + --control-md: 32px; + --control-sm: 28px; + --app-row: 36px; + --vx-motion: 150ms ease-out; + --font-display: var(--font-sans); + --font-sans: Geist, system-ui, sans-serif; + --font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace; +} +@media (max-width: 640px) { + :root { + --page-pad: 16px; + } +} +.vx-display { font-family: var(--font-display); font-size: 64px; line-height: 0.98; font-weight: 500; letter-spacing: -0.02em; } +.vx-h1 { font-family: var(--font-display); font-size: 48px; line-height: 1.05; font-weight: 500; letter-spacing: -0.02em; } +.vx-h2 { font-family: var(--font-display); font-size: 36px; line-height: 1.1; font-weight: 500; letter-spacing: -0.02em; } +.vx-h3 { font-family: var(--font-display); font-size: 28px; line-height: 1.15; font-weight: 500; letter-spacing: -0.02em; } +.vx-h4 { font-family: var(--font-display); font-size: 22px; line-height: 1.2; font-weight: 600; letter-spacing: -0.02em; } +.vx-h5 { font-family: var(--font-display); font-size: 18px; line-height: 1.25; font-weight: 600; letter-spacing: -0.01em; } +.vx-stat { font-family: var(--font-display); font-size: 30px; line-height: 1; font-weight: 500; letter-spacing: -0.02em; } +.vx-reading { font-family: var(--font-sans); font-size: 17px; line-height: 1.7; font-weight: 400; letter-spacing: 0; } +.vx-lede { font-family: var(--font-sans); font-size: 18px; line-height: 1.5; font-weight: 300; letter-spacing: -0.005em; } +.vx-body { font-family: var(--font-sans); font-size: 16px; line-height: 1.6; font-weight: 400; letter-spacing: 0; } +.vx-small { font-family: var(--font-sans); font-size: 14px; line-height: 1.5; font-weight: 400; letter-spacing: 0; } +.vx-caption { font-family: var(--font-sans); font-size: 12.5px; line-height: 1.4; font-weight: 400; letter-spacing: 0; } +.vx-display-mono { font-family: var(--font-mono); font-size: 40px; line-height: 1.1; font-weight: 500; letter-spacing: -0.04em; } +.vx-label { font-family: var(--font-mono); font-size: 12px; line-height: 1.3; font-weight: 500; letter-spacing: 0; } +.vx-label-sm { font-family: var(--font-mono); font-size: 11px; line-height: 1.3; font-weight: 500; letter-spacing: 0; } +.vx-mono { font-family: var(--font-mono); font-size: 13px; line-height: 1.6; font-weight: 400; letter-spacing: 0; } diff --git a/vortex-web/src/styles/vortex.css b/vortex-web/src/styles/vortex.css new file mode 100644 index 00000000000..ff6fac6e4e3 --- /dev/null +++ b/vortex-web/src/styles/vortex.css @@ -0,0 +1,760 @@ +/* Vortex design system components (sections: base, site, tables, app, focus), copied by tools/site-styles.mjs. + Edit the system, then run tools/site-styles.mjs; do not edit here. + Made by: node tools/site-styles.mjs --sections base,site,tables,app,focus --base + sha256 b4802f5118685e56475939eed3023f73d2de0096e7da25b39e2126b3e1c9180b */ + +/* ------------------------------------------------------------------ base */ +/* Every component sizes with border-box, so it draws the same under a site's reset or none. */ +:where([class*="vx-"]), +:where([class*="vx-"])::before, +:where([class*="vx-"])::after { + box-sizing: border-box; +} +/* Text for screen readers only, such as a copy button's "Copied". */ +.vx-sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} +/* The skip link, a page's first tab stop (WCAG 2.4.1): "Skip to content", to #main. Hidden as + vx-sr-only until it has focus, then at the top left above a sticky header, on ground in ink in + the label voice, with the shared focus ring. No script: the fragment moves focus to main, which + takes it (tabindex="-1"), so the next Tab starts in the content. Main draws no ring, the system's + or the browser's: around the whole page, it marks nothing a reader can use. */ +.vx-skip-link { + position: fixed; + top: var(--space-8); + left: var(--space-8); + z-index: 1200; + padding: var(--space-8) var(--space-12); + border-radius: var(--radius-control); + background: var(--ground); + color: var(--ink); + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.3; + font-weight: 500; + text-decoration: none; +} +.vx-skip-link:not(:focus) { + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} +main[tabindex="-1"]:focus { + outline: none; +} +.vx-card { + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-card); +} +.vx-label, +.vx-label-sm { + color: var(--ink-muted); +} +/* A machine reference, such as an error digest or a commit hash, in a label or in text. It keeps + its case, selects whole for a bug report, and breaks anywhere rather than overflow a phone. */ +.vx-ref { + font-family: var(--font-mono); + font-size: inherit; + text-transform: none; + letter-spacing: 0; + user-select: all; + overflow-wrap: anywhere; +} +.vx-stat, +.vx-num { + font-variant-numeric: tabular-nums; +} +/* In prose, the link rule styles an author's links. A system link (a vx- class) has its own rule. */ +.vx-link, +.vx-prose a:not([class*="vx-"]) { + color: var(--ink); + text-decoration: underline; + text-decoration-thickness: 1px; + text-decoration-color: color-mix(in srgb, var(--ink) 30%, transparent); + text-underline-offset: 3px; + transition: color var(--vx-motion), text-decoration-color var(--vx-motion); +} +.vx-link:hover, +.vx-prose a:not([class*="vx-"]):hover { + color: var(--accent-ink); + text-decoration-color: currentColor; +} +/* A quiet link in mono: header and footer navigation, feeds, source lines. */ +.vx-nav-link { + display: inline-flex; + align-items: center; + gap: var(--space-4); + color: var(--ink-muted); + text-decoration: none; + white-space: nowrap; + transition: color var(--vx-motion); +} +.vx-nav-link:hover, +.vx-nav-link[aria-current="page"], +.vx-nav-link[aria-current="true"] { + color: var(--ink); +} +.vx-nav-link svg { + width: 16px; + height: 16px; + fill: currentColor; +} +/* A source line: what a figure was read from, and when. */ +.vx-source { + display: inline-flex; + flex-wrap: wrap; + white-space: normal; + align-items: center; + gap: var(--space-4); +} + +/* Headings take ink, so a page never has to say so. It sits in base, which every site + copies. :where() keeps it at no specificity, so any component that colours its own headings wins. */ +:where(.vx-display, .vx-display-mono, .vx-h1, .vx-h2, .vx-h3, .vx-h4, .vx-h5, .vx-stat) { color: var(--ink); } + + +/* An encoding chain: mono steps on raised, joined by arrows. A post and DetailStrip + both draw it, so it sits in base, which every site copies. */ +.vx-chain { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-8); font: 400 12.5px/1 var(--font-mono); color: var(--ink-body); } +.vx-chain-step { padding: var(--space-4) var(--space-8); background: var(--raised); border: 1px solid var(--edge); border-radius: var(--radius-control); } +.vx-chain-arrow { color: var(--ink-muted); } + +/* ------------------------------------------------------------------ site */ +/* Page: the frame and the bands a page is built from. .vx-page holds the header, a main and the + footer in a column, so the footer sits at the bottom of a short page. A .vx-band is one slice of + the page, at page-max with page-pad on both sides, as the header and footer are. */ +.vx-page { min-height: 100dvh; display: flex; flex-direction: column; } +.vx-page > main { flex: 1 0 auto; } +.vx-band { width: 100%; max-width: var(--page-max); margin-inline: auto; padding: var(--space-80) var(--page-pad); } +.vx-band.is-compact { padding-block: var(--space-48); } +@media (min-width: 761px) { + .vx-band { padding-block: var(--space-152); } + .vx-band.is-compact { padding-block: var(--space-120); } +} +/* A page head: an Eyebrow, the heading and a lede, at the reading measure. */ +.vx-page-head { display: grid; justify-items: start; gap: var(--space-18); max-width: 840px; } +.vx-page-head > * { margin: 0; } +.vx-page-head > .vx-lede { color: var(--ink-body); } +/* The frame: the terminal's dashed rule, 1px wide, 12px on and 4px off, in ink-body. The header + draws it below itself and the footer above itself, on the side that faces the page, and a + .vx-frame box draws it on all four, over the ground, so it can sit on the ASCII mark. Inside a + frame, the tabs' rule is dashed too. */ +:where(.vx-header, .vx-footer, .vx-frame) { + --vx-dash-x: linear-gradient(90deg, var(--ink-body) 12px, transparent 12px); + --vx-dash-y: linear-gradient(180deg, var(--ink-body) 12px, transparent 12px); +} +.vx-frame { + background-color: var(--ground); + background-image: var(--vx-dash-x), var(--vx-dash-x), var(--vx-dash-y), var(--vx-dash-y); + background-size: 16px 1px, 16px 1px, 1px 16px, 1px 16px; + background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; + background-position: left top, left bottom, left top, right top; +} +.vx-frame .vx-tabs-list { border-bottom: 0; background: var(--vx-dash-x) left bottom / 16px 1px repeat-x; } +/* Two columns on the 24px gutter. They stack at 760px. */ +.vx-split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gutter); align-items: start; } +@media (max-width: 760px) { + .vx-split { grid-template-columns: minmax(0, 1fr); } +} +.vx-btn { + display: inline-flex; + align-items: center; + gap: var(--space-8); + height: var(--control-md); + padding: 0 var(--space-16); + border: 1px solid transparent; + border-radius: var(--radius-control); + font: 500 14px/1 var(--font-sans); + color: var(--ink); + background: transparent; + cursor: pointer; + text-decoration: none; + white-space: nowrap; + transition: background-color var(--vx-motion), border-color var(--vx-motion); +} +.vx-btn svg { width: 16px; height: 16px; fill: currentColor; } +/* Disabled: no fill, a line border, and muted text, so it reads as off in more than one way. */ +.vx-btn[disabled], .vx-btn[aria-disabled="true"] { background: transparent; border-color: var(--line); color: var(--ink-muted); cursor: not-allowed; } +.vx-btn-primary { background: var(--primary); color: var(--on-primary); } +.vx-btn-secondary { border-color: var(--edge); } +.vx-btn-secondary:hover { border-color: var(--ink); } +.vx-btn-ghost:hover { background: var(--raised); } +/* Pressed: one step past hover, in place. */ +.vx-btn-primary:active:not([disabled], [aria-disabled="true"]) { background: color-mix(in srgb, var(--primary) 82%, var(--ground)); } +.vx-btn-secondary:active:not([disabled], [aria-disabled="true"]) { background: var(--raised); } +.vx-btn-ghost:active:not([disabled], [aria-disabled="true"]) { background: var(--edge); } +.vx-btn.is-mono { padding: 0 var(--space-12); font: 400 13px/1 var(--font-mono); } +.vx-btn-count { display: inline-flex; align-items: center; gap: var(--space-4); padding-left: var(--space-12); border-left: 1px solid var(--edge); color: var(--ink-muted); } +/* Buttons side by side, primary first, wrapping onto a second row on a phone. */ +.vx-btn-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-12); } + +/* A 404 or error page: the hero with no bands under it. It takes the height its parent (a flex + column between the header and the footer) leaves, and centres its text in it. With the mark it + takes the homepage hero's two columns, and stacks below 1100px as the hero does. */ +.vx-status { flex: 1 0 auto; display: grid; grid-template-columns: minmax(0, 760px); align-content: center; gap: var(--space-48); padding: var(--space-32) var(--page-pad); } +.vx-status.has-mark { grid-template-columns: minmax(0, 760px) minmax(320px, 1fr); align-content: normal; } +.vx-status-text { display: grid; align-content: center; justify-items: start; gap: var(--space-24); } +.vx-status-text > * { margin: 0; } +/* The homepage's voice: a mono headline (vx-display-mono) and a mono sentence at the standfirst's size. */ +.vx-status-sentence { font: 400 16px/1.6 var(--font-mono); color: var(--ink-body); } +/* The 404's headline is its code: mono at the display size, so the page has one large thing to read. */ +.vx-status-code { font: 500 64px/0.98 var(--font-mono); letter-spacing: -0.04em; color: var(--ink); } +/* The mark's cell has a height to fill. Its host is never taller than 9/10 of the cell's width. */ +.vx-status-mark { position: relative; min-height: 0; container-type: size; } +.vx-status-mark > * { position: absolute; inset: 0; height: min(100cqh, 90cqw); margin: auto 0; } +@media (max-width: 1099px) { + .vx-status.has-mark { grid-template-columns: minmax(0, 1fr); align-content: center; gap: var(--space-24); } + .vx-status-mark { height: 320px; } +} +@media (max-width: 640px) { + .vx-status-mark { height: 240px; } +} + +/* A text field: raised, at control-md, with an edge that turns ink on hover, like the docs' search. + A row holds a field and its button. */ +.vx-field { + appearance: none; + height: var(--control-md); + margin: 0; + padding: 0 var(--space-12); + font: 400 14px/1 var(--font-sans); + color: var(--ink); + background: var(--raised); + border: 1px solid var(--edge); + border-radius: var(--radius-control); + transition: border-color var(--vx-motion); +} +.vx-field::placeholder { color: var(--ink-muted); opacity: 1; } +.vx-field:hover { border-color: var(--ink); } +.vx-field[aria-invalid="true"] { border-color: var(--error-ink); } +.vx-field:disabled { background: transparent; border-color: var(--line); color: var(--ink-muted); cursor: not-allowed; } +/* A compact field, for a pane's head or a toolbar. */ +.vx-field.is-sm { height: var(--control-sm); font-size: 13px; } +.vx-field-row { display: flex; gap: var(--space-8); max-width: 440px; } +.vx-field-row > .vx-field { flex: 1 1 auto; min-width: 0; } +.vx-field-error { margin: var(--space-8) 0 0; color: var(--error-ink); } + +/* The header: the wordmark, then either the site's navigation or the docs' label and search, + then the end group. */ +.vx-header { + display: flex; + align-items: center; + gap: var(--space-24); + height: 64px; + margin-top: var(--space-16); + padding: 0 var(--page-pad); + background-color: var(--ground); + background-image: var(--vx-dash-x); + background-size: 16px 1px; + background-repeat: repeat-x; + background-position: left bottom; + position: relative; +} +.vx-header-start { display: flex; flex-shrink: 0; align-items: center; gap: var(--space-12); } +/* On one row, 32px set the wordmark and the end group apart from the links. */ +@media (min-width: 801px) { + .vx-header-start { padding-right: var(--space-32); } + .vx-header-end { padding-left: var(--space-32); } +} +.vx-header-mark { height: 24px; width: auto; display: block; } +.vx-header-mark.is-light, [data-theme="light"] .vx-header-mark.is-dark { display: none; } +[data-theme="light"] .vx-header-mark.is-light { display: block; } +/* A page set to System follows the OS: under its light, the marks swap as in Light. */ +@media (prefers-color-scheme: light) { + [data-theme="system"] .vx-header-mark.is-dark { display: none; } + [data-theme="system"] .vx-header-mark.is-light { display: block; } +} +.vx-header-label { margin: 0; padding-left: var(--space-12); border-left: 1px solid var(--edge); line-height: 16px; } +/* The links follow the wordmark and the end group takes the slack, so the links stay put when the + end group's width changes (the blog's ThemeSwitch). */ +.vx-header-nav { display: flex; gap: var(--space-24); } +.vx-header-end { display: flex; align-items: center; gap: var(--space-16); margin-left: auto; } +.vx-header-nav + .vx-header-end { margin-left: auto; } +@media (max-width: 760px) { + .vx-btn-count { display: none; } +} +/* The menu: on a narrow screen the site's links go behind a Menu button at the end of the row, a +
, so it opens without scripts. Its panel is a popover under the button, on the ground with + the buttons' solid edge: it covers what is under it, and never reads as one of the page's dashed + frames. */ +.vx-header-menu { display: none; } +.vx-header-menu > summary { list-style: none; } +.vx-header-menu > summary::-webkit-details-marker { display: none; } +.vx-header-menu > summary .is-close, .vx-header-menu[open] > summary .is-open { display: none; } +.vx-header-menu[open] > summary .is-close { display: block; } +.vx-header-menu[open] > summary { border-color: var(--ink); } +.vx-header-menu-panel { + position: absolute; + z-index: 10; + top: calc(100% + var(--space-8)); + right: var(--page-pad); + display: grid; + width: 200px; + padding: var(--space-4) 0; + background: var(--ground); + border: 1px solid var(--edge); + border-radius: var(--radius-card); +} +.vx-header-menu-panel .vx-nav-link { display: flex; min-height: 44px; padding: 0 var(--space-16); } +.vx-header-menu-panel .vx-nav-link:hover { background: var(--wash); } +/* The site header needs about 790px on one row: the wordmark, five links and the GitHub button + with its count. Below 800px the links go into the menu. With the ThemeSwitch in its end group the + row needs about 900px, so they go below 920px. */ +@media (max-width: 800px) { + .vx-header.has-nav .vx-header-nav { display: none; } + .vx-header.has-nav .vx-header-menu { display: block; } + .vx-header.has-nav .vx-header-end { margin-left: auto; } +} +@media (max-width: 920px) { + .vx-header.has-nav:has(.vx-theme-switch) .vx-header-nav { display: none; } + .vx-header.has-nav:has(.vx-theme-switch) .vx-header-menu { display: block; } + .vx-header.has-nav:has(.vx-theme-switch) .vx-header-end { margin-left: auto; } +} +/* On a phone the row is the wordmark, the GitHub mark, the switch and the menu, closer together. + With the switch the Menu button keeps its icon and drops its word below 480px. */ +@media (max-width: 480px) { + .vx-header { gap: var(--space-16); } + .vx-header-end { gap: var(--space-8); } + .vx-header-end:has(.vx-theme-switch) .vx-header-menu-text { + position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; + } +} +@media (max-width: 640px) { + .vx-header-label, .vx-header-end .is-wide { display: none; } +} +/* With the Menu button, the wordmark, the GitHub button with the repository's name and the button + need about 390px on one row, and about 500px with the switch. So the button drops the name and + keeps its mark below 480px, or 560px with the switch. Its aria-label still names the repository. */ +@media (max-width: 560px) { + .vx-header-end:has(.vx-theme-switch) .vx-header-repo { display: none; } +} +@media (max-width: 480px) { + .vx-header-repo { display: none; } +} +/* A display line never strands one word on its last line, and on phones its lines balance. */ +.vx-display { text-wrap: pretty; } +/* On phones it sets at the h1 size, where 64px would take four lines. */ +@media (max-width: 640px) { + .vx-display { font-size: 48px; text-wrap: balance; } +} + +.vx-footer { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-16) var(--space-24); + margin-bottom: var(--space-16); + padding: var(--space-16) var(--page-pad); + background-image: var(--vx-dash-x); + background-size: 16px 1px; + background-repeat: repeat-x; + background-position: left top; + font: 400 11px/1.5 var(--font-mono); + letter-spacing: 0.02em; /* the one tracked style: small print */ + color: var(--ink-muted); +} +.vx-footer-mark { height: 12px; width: auto; opacity: 0.85; } +[data-theme="light"] .vx-footer-mark { filter: invert(1); } +@media (prefers-color-scheme: light) { + [data-theme="system"] .vx-footer-mark { filter: invert(1); } +} +.vx-footer-text { flex: 1 1 420px; min-width: 0; margin: 0; } +.vx-footer-credit { white-space: nowrap; } +.vx-footer-nav { display: flex; flex-wrap: wrap; gap: var(--space-8) var(--space-16); } +/* The solid variant, for a tool such as bench, whose own controls and tables sit under the header: + the same header and footer, with solid rules in line, flush to the window's edges, and a rule + only on the side that faces the page. */ +.vx-header.is-solid, .vx-footer.is-solid { --vx-dash-x: linear-gradient(var(--line), var(--line)); } +.vx-header.is-solid { margin-top: 0; background-image: var(--vx-dash-x); background-position: left bottom; } +.vx-footer.is-solid { margin-bottom: 0; background-image: var(--vx-dash-x); background-position: left top; } +/* The tools variant, for a tool such as bench: one sticky row holds the wordmark, the tool's label, + its own controls (.vx-header-tools) and the end group. The site's links are always behind the + Menu button, so the controls have the room. The label can be the page's h1, so it takes no margin. */ +.vx-header.has-tools { position: sticky; top: 0; z-index: 1000; } +/* The sticky header must not cover what takes focus or what a link scrolls to (WCAG 2.4.11). + The header is 64px tall. */ +html:has(.vx-header.has-tools) { scroll-padding-top: calc(var(--space-64) + var(--space-8)); } +/* No divider beside the label: it reads as part of the wordmark. */ +.vx-header.has-tools .vx-header-label { border-left: 0; padding-left: 0; } +.vx-header.has-tools .vx-header-nav { display: none; } +.vx-header.has-tools .vx-header-menu { display: block; } +.vx-header-tools { display: flex; align-items: center; gap: var(--space-12); margin-left: auto; min-width: 0; } +/* The end group's spill items (.vx-header-spill), such as a theme toggle, have a twin row in the + Menu's panel (.vx-header-menu-spill), shown only when the item moves there. */ +.vx-header-menu-panel .vx-header-menu-spill { display: none; } +.vx-header-menu-panel button.vx-nav-link { + appearance: none; + background: none; + border: 0; + width: 100%; + text-align: start; + cursor: pointer; +} +/* Below 960px the row keeps the controls: the label leaves the view but stays the page's heading, + and the GitHub button keeps only its mark. */ +@media (max-width: 960px) { + .vx-header.has-tools .vx-header-label { + display: block; position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; + } + .vx-header.has-tools .vx-header-repo, .vx-header.has-tools .vx-btn-count { display: none; } + .vx-header.has-tools .vx-header-tools { gap: var(--space-8); } +} +/* On a phone the spill items move into the Menu's panel, under the links, and the Menu button + keeps only its icon, so the row stays one row down to 320px. */ +@media (max-width: 640px) { + .vx-header.has-tools { gap: var(--space-12); } + .vx-header.has-tools .vx-header-end > .vx-header-spill { display: none; } + .vx-header.has-tools .vx-header-menu-spill { + display: flex; align-items: center; gap: var(--space-8); border-top: 1px solid var(--line); + } + .vx-header.has-tools .vx-header-menu-spill svg { width: 16px; height: 16px; flex-shrink: 0; } + .vx-header.has-tools .vx-header-menu-text { + position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; + } +} + +/* The mono headline: tight, as the old terminal set it, and at the h3 size on phones. */ +.vx-display-mono { text-wrap: pretty; } +@media (max-width: 640px) { + .vx-display-mono { font-size: 28px; } +} +@media (max-width: 640px) { .vx-status-code { font-size: 48px; } } + +/* The results table: every system's figure for each benchmark, in a frame. Vortex's column is Blue, + the best figure in each row is underlined, as a model card marks its best score, and the last + column says how Vortex does against Parquet. Below 760px only Vortex and the verdict show. */ +.vx-results { padding: var(--space-18) var(--space-24); font: 400 13px/1.6 var(--font-mono); color: var(--ink-body); } +.vx-results-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-4) var(--space-16); margin: 0 0 var(--space-12); color: var(--ink); } +/* The source line: bench's link and the read date, which wrap as one line each. */ +.vx-results-src { display: inline-flex; flex-wrap: wrap; gap: 0 var(--space-8); color: var(--ink-muted); } +.vx-results-src > * { white-space: nowrap; } +/* The foot: the key, then a note for each panel with a range, one per line. */ +.vx-results-foot { display: grid; gap: var(--space-4); margin: var(--space-12) 0 0; color: var(--ink-muted); } +.vx-results-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; } +/* The cells do not wrap, so a table wider than its frame scrolls inside it, not off the page. */ +.vx-results-wrap { overflow-x: auto; } +.vx-results-table th, +.vx-results-table td { padding: var(--space-4) 0 var(--space-4) var(--space-16); text-align: right; white-space: nowrap; font-weight: 400; } +.vx-results-table th:first-child { padding-left: 0; text-align: left; } +.vx-results-table thead th { padding-bottom: var(--space-8); color: var(--ink-muted); } +.vx-results-table tbody th { color: var(--ink-body); } +.vx-results-table .is-ours { color: var(--accent-ink); } +.vx-results-table .is-missing { color: var(--ink-muted); } +/* The underline is ink on every column: it names the result, and Blue names Vortex. */ +.vx-results-table .is-best { color: var(--ink); font-weight: 600; text-decoration: underline 2px var(--ink); text-underline-offset: 5px; } +.vx-results-table .is-best.is-ours { color: var(--accent-ink); } +.vx-results-table .is-verdict { padding-left: var(--space-24); color: var(--ink); } +@media (max-width: 760px) { + .vx-results { padding: var(--space-16); } + .vx-results-table :is(th, td):not(:first-child, .is-ours, .is-verdict) { display: none; } +} +/* The narrowest phones, 320px: the three columns fit inside the frame with 12px of padding, 12px between them, and headings that wrap. */ +@media (max-width: 400px) { + .vx-results { padding: var(--space-12); } + .vx-results-table :is(th, td):not(:first-child) { padding-left: var(--space-12); } + .vx-results-table th { white-space: normal; } +} + +.vx-eyebrow { display: inline-flex; align-items: center; gap: var(--space-8); } +.vx-eyebrow::before { content: ""; width: 6px; height: 6px; background: var(--ink-muted); flex: none; } + +.vx-tabs { overflow: hidden; } +.vx-tabs-list { display: flex; padding: 0 var(--space-4); border-bottom: 1px solid var(--line); overflow-x: auto; } +.vx-tabs-list button { + appearance: none; + margin: 0 0 -1px; + padding: var(--space-12) var(--space-12) var(--space-8); + border: 0; + border-bottom: 2px solid transparent; + background: none; + cursor: pointer; + color: var(--ink-muted); +} +.vx-tabs-list button:hover { color: var(--ink-body); } +/* Six tabs fit a phone: the side padding steps down to 8px below 480px and 4px below 400px. + Narrower than about 330px, the list scrolls. */ +@media (max-width: 480px) { + .vx-tabs-list button { padding-inline: var(--space-8); } +} +@media (max-width: 400px) { + .vx-tabs-list button { padding-inline: var(--space-4); } +} +/* A selected tab takes Blue's rule, as every selection does. */ +.vx-tabs-list button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); } +/* After the selected rule, so a press shows on the selected tab too. */ +.vx-tabs-list button:active { background: var(--wash); } +.vx-tabs-code { display: flex; align-items: flex-start; gap: var(--space-12); padding: var(--space-12) var(--space-16); } +/* Every panel shares one grid cell and only the selected one shows, so the block is always the + tallest panel's height and switching tabs never moves the page. */ +.vx-tabs-panels { display: grid; flex: 1; min-width: 0; } +.vx-tabs-panels > .vx-tabs-pre { grid-area: 1 / 1; } +.vx-tabs-pre { margin: 0; flex: 1; min-width: 0; font: 400 13px/1.6 var(--font-mono); color: var(--ink-body); white-space: pre-wrap; overflow-wrap: anywhere; } +.vx-tabs-pre.is-idle { visibility: hidden; } +.vx-prompt { color: var(--ink-muted); user-select: none; } +.vx-copy { all: unset; cursor: pointer; color: var(--ink-muted); padding: 2px; } +.vx-copy:hover { color: var(--ink); } +.vx-copy:active { background: var(--wash); } +.vx-copy svg { width: 16px; height: 16px; fill: currentColor; display: block; } +.vx-copy .is-check, .vx-copy.is-done .is-copy { display: none; } +.vx-copy.is-done .is-check { display: block; } + +/* A bar chart's rows: name, a bar on its track, the value. Vortex is the one Blue row. renderBars + sets --bar-value to fit the longest value, so every track has one length and the bars share a scale. */ +.vx-bars { display: grid; gap: var(--space-8); } +.vx-bar { + display: grid; + grid-template-columns: var(--bar-name, 56px) minmax(0, 1fr) var(--bar-value, auto); + gap: var(--space-8); + align-items: center; + font: 400 12px/1 var(--font-mono); + font-variant-numeric: tabular-nums; + color: var(--ink-muted); +} +.vx-bar-value { text-align: right; } +.vx-bar-missing { grid-column: 2 / -1; } +.vx-bar-extra { margin-left: var(--space-8); } +.vx-bar-track { height: 6px; background: var(--chart-track); } +.vx-bar-fill { display: block; height: 100%; background: var(--chart-muted); } +.vx-bar.is-ours { color: var(--accent-ink); } +.vx-bar.is-ours .vx-bar-fill { background: var(--chart-vortex); } +/* Parquet, Lance and Arrow take muted tints of their colours, so the bars are not grey on grey, + drawn at 42% so that Vortex's Blue is the one solid bar and leads even where the others are + longer. Every bar prints its value, so the bar is never the only way to read it. */ +.vx-bar.is-parquet .vx-bar-fill { background: var(--chart-parquet); } +.vx-bar.is-lance .vx-bar-fill { background: var(--chart-lance); } +.vx-bar.is-arrow .vx-bar-fill { background: var(--chart-arrow); } +.vx-bar:is(.is-parquet, .is-lance, .is-arrow) .vx-bar-fill { opacity: 0.42; } +.vx-bars.is-lg .vx-bar { gap: var(--space-12); font-size: 12.5px; } +.vx-bars.is-lg .vx-bar-track { height: 10px; } + +/* A fieldset, so its margin and minimum width are cleared. Until a script presses a button, the + choice THEME_BOOTSTRAP wrote to data-theme-choice on the root shows pressed. */ +/* The text switch (vx-switch) draws the theme switch's group, with words on its buttons. */ +.vx-theme-switch, +.vx-switch { + display: inline-flex; + gap: 2px; + min-width: 0; + margin: 0; + padding: 2px; + background: var(--raised); + border: 1px solid var(--edge); + border-radius: var(--radius-control); +} +.vx-theme-switch button { + all: unset; + box-sizing: border-box; + cursor: pointer; + display: grid; + place-items: center; + width: 26px; + height: 26px; + border-radius: var(--radius-control); + color: var(--ink-muted); + transition: color var(--vx-motion), background-color var(--vx-motion); +} +.vx-switch button { + all: unset; + box-sizing: border-box; + cursor: pointer; + height: 26px; + padding: 0 var(--space-12); + border-radius: var(--radius-control); + font: 400 12.5px/26px var(--font-sans); + white-space: nowrap; + color: var(--ink-muted); + transition: color var(--vx-motion), background-color var(--vx-motion); +} +.vx-theme-switch button:hover, +.vx-switch button:hover { color: var(--ink); } +.vx-theme-switch button[aria-pressed="true"], +.vx-switch button[aria-pressed="true"], +:root[data-theme-choice="light"] .vx-theme-switch:not(:has([aria-pressed="true"])) button[data-choice="light"], +:root[data-theme-choice="dark"] .vx-theme-switch:not(:has([aria-pressed="true"])) button[data-choice="dark"], +:root[data-theme-choice="system"] .vx-theme-switch:not(:has([aria-pressed="true"])) button[data-choice="system"] { background: var(--surface); color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); } +/* After the pressed rule, so a press shows on the current choice too. */ +.vx-theme-switch button:active, +.vx-switch button:active { background: var(--edge); } +.vx-theme-switch svg { width: 16px; height: 16px; fill: currentColor; } + +/* ------------------------------------------------------------------ tables + A post's tables, which Markdown writes, and data tables. The blog and the Explorer both copy it. */ +.vx-table-wrap { overflow-x: auto; } +.vx-prose table, +.vx-table { width: 100%; border-collapse: collapse; font: 400 14px/1.4 var(--font-sans); font-variant-numeric: tabular-nums; } +.vx-prose th, +.vx-table th { + padding: var(--space-8) var(--space-12) var(--space-8) 0; + text-align: left; + font: 500 11px/1.3 var(--font-mono); + color: var(--ink-muted); + border-bottom: 1px solid var(--edge); +} +.vx-prose td, +.vx-table td { padding: var(--space-8) var(--space-12) var(--space-8) 0; border-bottom: 1px solid var(--line); } +.vx-prose .is-num, +.vx-table .is-num { text-align: right; white-space: nowrap; } +/* The figures are mono at 13px; the column's head keeps the heads' 11px. */ +.vx-prose td.is-num, +.vx-table td.is-num { font-family: var(--font-mono); font-size: 13px; } +.vx-prose tr.is-total td, +.vx-table tr.is-total td { color: var(--ink); font-weight: 600; border-bottom: 0; } +/* Data tables: every cell in mono, rows at app height, and column names keep their case. */ +.vx-table.is-data th, +.vx-table.is-data td { padding: var(--space-8) var(--space-12); } +.vx-table.is-data td { height: var(--app-row); box-sizing: border-box; white-space: nowrap; font-family: var(--font-mono); font-size: 13px; color: var(--ink-body); } +.vx-table th.is-selected { color: var(--accent-ink); } +.vx-table td.is-selected { background: var(--accent-soft); color: var(--ink); } +/* A data table on a surface keeps its head in view and washes the row under the pointer. A spacer + row (is-spacer) stands for rows a virtual list has not drawn. */ +.vx-table.is-data thead th { position: sticky; top: 0; z-index: 1; background: var(--surface); } +.vx-table.is-data tbody tr:not(.is-spacer):hover td { background: var(--wash); } +.vx-table.is-data tr.is-spacer td { height: auto; padding: 0; border: 0; } + +/* ------------------------------------------------------------------ app: Explorer */ +/* Chips filter a view. A pressed chip is a selected item: an accent-soft wash and an accent-ink border, which holds 3:1. */ +.vx-chips { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-8); } +.vx-chip { + appearance: none; + display: inline-flex; + align-items: center; + height: var(--control-sm); + margin: 0; + padding: 0 var(--space-12); + background: var(--raised); + border: 1px solid var(--edge); + border-radius: var(--radius-control); + font: 400 12.5px/1 var(--font-sans); + color: var(--ink-body); + cursor: pointer; + transition: border-color var(--vx-motion), color var(--vx-motion), background-color var(--vx-motion); +} +.vx-chip:hover { border-color: var(--ink); color: var(--ink); } +.vx-chip[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-ink); color: var(--ink); } +/* After the pressed rule, so a press shows on a chip that is already on. */ +.vx-chip:active { background: var(--edge); } +.vx-chip.is-all { font-weight: 500; } + +.vx-tree { padding: var(--space-8) 0; font: 400 12.5px/1 var(--font-mono); } +.vx-tree-node { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: var(--space-12); + align-items: center; + min-height: var(--control-sm); + padding: var(--space-4) var(--space-16); + /* A layout is as deep as its file: each level steps in by space-16. The consumer sets + --vx-depth on the node (0 at the top), beside its aria-level. */ + padding-left: calc(var(--space-12) + var(--vx-depth, 0) * var(--space-16)); + color: var(--ink-body); + cursor: pointer; + transition: background-color var(--vx-motion); +} +/* A closed parent hides the nodes under it with the hidden attribute, which display: grid overrides. */ +.vx-tree-node[hidden] { display: none; } +.vx-tree-node:hover { background: var(--wash); } +.vx-tree-node[aria-selected="true"] { background: var(--accent-soft); color: var(--ink); box-shadow: inset 2px 0 0 var(--accent); } +.vx-tree-node.is-nested { --vx-depth: 1; } +.vx-tree-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.vx-tree-type { color: var(--ink-muted); } +.vx-tree-size { color: var(--ink-muted); font-variant-numeric: tabular-nums; } +.vx-tree-dot { display: inline-block; width: 8px; height: 8px; margin-right: var(--space-8); vertical-align: -1px; background: var(--vx-dt, var(--chart-muted)); } +/* A parent leads with a chevron (chevron_right) that turns down when it is open. A leaf keeps the + space with an empty toggle, so names at one depth line up. */ +.vx-tree-toggle { display: inline-block; width: 16px; height: 16px; margin-right: var(--space-4); vertical-align: -4px; fill: var(--ink-muted); transition: rotate var(--vx-motion); } +.vx-tree-node[aria-expanded="true"] > .vx-tree-name > .vx-tree-toggle, +[aria-expanded="true"] > .vx-tree-toggle { rotate: 90deg; } +.vx-tree-toggle.is-leaf { visibility: hidden; } +.vx-tree-node:hover .vx-tree-toggle { fill: var(--ink); } + +/* A selected segment carries an accent ring, and the others step back. */ +.vx-filemap { display: grid; gap: var(--space-8); } +.vx-filemap-bar { display: flex; height: 34px; border: 1px solid var(--edge); border-radius: var(--radius-control); overflow: hidden; } +.vx-filemap-bar > button { all: unset; box-sizing: border-box; display: block; height: 100%; border-right: 1px solid var(--surface); background: var(--vx-dt-fill); box-shadow: inset 0 0 0 1px var(--vx-dt-edge); cursor: pointer; transition: opacity var(--vx-motion); } +.vx-filemap-bar > button[aria-pressed="true"] { box-shadow: inset 0 0 0 2px var(--accent); } +.vx-filemap-bar:has(> [aria-pressed="true"]) > button:not([aria-pressed="true"]) { opacity: 0.4; } +.vx-filemap-axis { display: flex; justify-content: space-between; } +.vx-filemap-legend { display: flex; flex-wrap: wrap; gap: var(--space-8) var(--space-16); font: 400 12.5px/1.3 var(--font-sans); color: var(--ink-muted); } +.vx-filemap-swatch { display: inline-block; width: 10px; height: 10px; margin-right: var(--space-8); vertical-align: -1px; background: var(--vx-dt, var(--chart-muted)); } + +/* Dtype colours: one per dtype family (components/lib/dtype.js names the family), shared by the + tree's dots, the file map, and every Explorer view. Five families take chart-2 to chart-6, never + Blue. A list or struct is muted, because its children carry their own colours. The dtype's name + always shows beside its colour, so the colour is never the only cue. */ +.vx-dt-int { --vx-dt: var(--chart-5); } +.vx-dt-float { --vx-dt: var(--chart-2); } +.vx-dt-utf8 { --vx-dt: var(--chart-6); } +.vx-dt-bool { --vx-dt: var(--chart-4); } +.vx-dt-datetime { --vx-dt: var(--chart-3); } +.vx-dt-list { --vx-dt: var(--chart-muted); } +.vx-dt-struct { --vx-dt: var(--chart-muted); } +.vx-dt-other { --vx-dt: var(--chart-muted); } +/* A filled block of a dtype: solid in Dark, with the surface as the gap between blocks; a tint in + Light, with an edge in the full hue. A label on it takes --vx-dt-ink: chart-fill-ink on a hue, ink + on a muted family's grey. */ +.vx-dt-int, .vx-dt-float, .vx-dt-utf8, .vx-dt-bool, .vx-dt-datetime, .vx-dt-list, .vx-dt-struct, .vx-dt-other { + --vx-dt-fill: color-mix(in oklab, var(--vx-dt) var(--chart-fill-mix), var(--surface)); + --vx-dt-edge: color-mix(in oklab, var(--vx-dt) var(--chart-edge-mix), var(--surface)); + --vx-dt-ink: var(--chart-fill-ink); +} +.vx-dt-list, .vx-dt-struct, .vx-dt-other { --vx-dt-ink: var(--ink); } + +/* Pane: a card that holds one view of an app, with a head of controls, a body that scrolls, and an + optional foot. The card draws the frame; the pane lays it out. */ +.vx-pane { display: flex; flex-direction: column; min-height: 0; overflow: hidden; } +.vx-pane-head { + flex: none; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-8) var(--space-12); + padding: var(--space-12) var(--space-16); + border-bottom: 1px solid var(--line); +} +.vx-pane-head > .vx-label { margin-right: auto; } +.vx-pane-body { flex: 1 1 auto; min-height: 0; overflow: auto; scrollbar-width: thin; scrollbar-color: var(--edge) transparent; } +.vx-pane-foot { flex: none; padding: var(--space-8) var(--space-16); border-top: 1px solid var(--line); } + +/* Facts: a key and value list (dl). Each pair is a div of a dt, set as vx-label-sm in the markup, + and a dd in ink mono. is-row lays the pairs in one line that wraps. */ +.vx-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-12) var(--space-16); margin: 0; padding: var(--space-16); } +.vx-facts.is-row { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); } +.vx-facts > div { display: grid; gap: var(--space-4); min-width: 0; } +.vx-facts dt { margin: 0; } +.vx-facts dd { margin: 0; color: var(--ink); font: 400 13px/1.4 var(--font-mono); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; } + +/* The detail strip measures its own width, so it lays out the same in any column. */ +.vx-detail { container-type: inline-size; overflow: hidden; } +.vx-detail-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-12); padding: var(--space-12) var(--space-16); border-bottom: 1px solid var(--line); } +.vx-detail-name { color: var(--ink); } +.vx-detail-cells { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr) minmax(0, 1.6fr); gap: 1px; background: var(--line); } +.vx-detail-cell { display: grid; gap: var(--space-8); align-content: start; padding: var(--space-12) var(--space-16); background: var(--surface); } +.vx-detail-value { color: var(--ink); } +@container (max-width: 520px) { + .vx-detail-cells { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .vx-detail-cell:last-child { grid-column: 1 / -1; } +} + +.vx-status-line { display: flex; flex-wrap: wrap; gap: var(--space-8) var(--space-20); padding: var(--space-8) var(--space-20); border-top: 1px solid var(--line); } +.vx-status-item { display: inline-flex; align-items: center; gap: var(--space-8); } +.vx-status-item::before { content: ""; width: 6px; height: 6px; border-radius: var(--radius-full); background: var(--status-idle); } +.vx-status-item.is-ok::before { background: var(--status-ok); } +.vx-status-item.is-warn::before { background: var(--status-warn); } +.vx-status-item.is-error::before { background: var(--status-error); } + +/* ------------------------------------------------------------------ focus + One rule for every focusable element, last so that it wins over the resets above. The docs' + phone menu is a hidden checkbox, so its label shows the checkbox's ring. Rows and + segments inside a clipped container draw it inside their edge. */ +:is(a, button, input, select, textarea, summary, [tabindex]):focus-visible, +.vx-docs-toggle:focus-visible ~ .vx-docs-header .vx-docs-menu { outline: 2px solid var(--focus-ring); outline-offset: 2px; } +:is(.vx-docs-nav a, .vx-docs-nav summary, .vx-docs-toc a, .vx-docs-fold a, .vx-post-row, .vx-tree-node, .vx-filemap-bar > button, .vx-tabs-list button, .vx-switch button, .vx-theme-switch button, .vx-table-wrap :is(a, button), .vx-docs-main table.docutils a):focus-visible { outline-offset: -2px; } diff --git a/vortex-web/src/workers/VortexWorker.ts b/vortex-web/src/workers/VortexWorker.ts index 5fe90db9f6c..566d13d0826 100644 --- a/vortex-web/src/workers/VortexWorker.ts +++ b/vortex-web/src/workers/VortexWorker.ts @@ -24,7 +24,7 @@ interface PendingRequest { /** Typed async wrapper around the Vortex WASM Web Worker. */ export class VortexWorker { private worker: Worker; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // biome-ignore lint/suspicious/noExplicitAny: requests with different result types share one map. private pending = new Map>(); private nextId = 0; diff --git a/vortex-web/tsconfig.app.json b/vortex-web/tsconfig.app.json index 4d4a56c741e..b2d08b7f692 100644 --- a/vortex-web/tsconfig.app.json +++ b/vortex-web/tsconfig.app.json @@ -18,7 +18,9 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true }, "include": ["src"] } diff --git a/vortex-web/tsconfig.json b/vortex-web/tsconfig.json index 1ffef600d95..d32ff682003 100644 --- a/vortex-web/tsconfig.json +++ b/vortex-web/tsconfig.json @@ -1,7 +1,4 @@ { "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] } diff --git a/vortex-web/tsconfig.node.json b/vortex-web/tsconfig.node.json index 843c8a88dbe..568f83d3952 100644 --- a/vortex-web/tsconfig.node.json +++ b/vortex-web/tsconfig.node.json @@ -16,7 +16,9 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true }, - "include": ["vite.config.ts", "eslint.config.ts"] + "include": ["vite.config.ts"] } diff --git a/vortex-web/vite.config.ts b/vortex-web/vite.config.ts index 4c7d941bdc4..1cc531e2f2a 100644 --- a/vortex-web/vite.config.ts +++ b/vortex-web/vite.config.ts @@ -1,10 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react()], }); From dd2eeded4ae6e21ec14144f79090caf4c5ebd4aa Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 12:16:04 -0500 Subject: [PATCH 27/41] Refresh CodSpeed simulation counts and timings (#11231) ## Summary The snapshot used by #11180 predates the benchmark cleanup merges, so it overestimates simulation counts and target runtimes. Refresh both snapshots from the five successful simulation shards in [develop run 41869576169](https://github.com/vortex-data/vortex/actions/runs/41869576169) at `141abdc590`. Note that these are just heuristics, so this will never affect correctness / running of benchmarks. ## Changes Recorded simulation cases fall from 1,869 to 1,408, and total target execution time falls from 711s to 400s. The refreshed plan estimates 83-84s for each regular shard and 65s for the allocation-excluded shard. These are target execution estimates, not full CI job times. Verified complete measurements for all 129 targets, exact target coverage, the 800-benchmark cap, and allocation-mode separation. The planner and `git diff --check` passed. No local tests or benchmarks were run. The pre-commit and pre-push hooks were bypassed as requested. Signed-off-by: Connor Tsui --- scripts/codspeed_bench_counts.json | 28 +-- scripts/codspeed_bench_durations.json | 262 +++++++++++++------------- 2 files changed, 145 insertions(+), 145 deletions(-) diff --git a/scripts/codspeed_bench_counts.json b/scripts/codspeed_bench_counts.json index 87ccb9e575b..32222ba7fa8 100644 --- a/scripts/codspeed_bench_counts.json +++ b/scripts/codspeed_bench_counts.json @@ -6,7 +6,7 @@ "take_union": 4 }, "vortex-alp": { - "alp_compress": 96 + "alp_compress": 36 }, "vortex-array": { "aggregate_grouped": 18, @@ -16,20 +16,20 @@ "bool_zip": 2, "cast_decimal": 6, "cast_primitive": 6, - "chunk_array_builder": 32, + "chunk_array_builder": 31, "chunked_dict_builder": 24, "chunked_fsl_canonicalize": 6, "compare": 9, "compare_sorted": 3, "copy_to_canonical_width": 0, - "dict_compare": 27, - "dict_compress": 70, + "dict_compare": 16, + "dict_compress": 25, "dict_mask": 16, "dict_unreferenced_mask": 10, "expr_case_when": 28, "expr_large_struct_pack": 4, "expr_optimize": 1, - "filter_bool": 61, + "filter_bool": 47, "filter_fixed_width": 30, "filter_fsl": 4, "filter_list": 12, @@ -49,7 +49,7 @@ "row_fn_bool_retry": 0, "row_fn_output": 0, "row_fn_utf8_input": 0, - "run_end_compress": 77, + "run_end_compress": 35, "run_end_decode": 29, "run_end_decode_primitive": 12, "run_end_filter": 8, @@ -62,7 +62,7 @@ "scalar_subtract": 0, "search_sorted": 1, "slice_dict_primitive": 3, - "take_chunked": 27, + "take_chunked": 0, "take_filter": 32, "take_fsl": 12, "take_patches": 36, @@ -83,7 +83,7 @@ "cpu_dispatch": 6, "take_bits": 0, "vortex_bitbuffer": 58, - "vortex_buffer": 34 + "vortex_buffer": 29 }, "vortex-compressor": { "dict_encode": 0, @@ -95,7 +95,7 @@ "lane_kernels": 7 }, "vortex-datetime-parts": { - "split_temporal": 4 + "split_temporal": 6 }, "vortex-decimal-byte-parts": { "dbp_assemble": 0, @@ -109,12 +109,12 @@ "bitpack_blocked": 0, "bitpack_blocked_decompress": 0, "bitpack_compare": 2, - "bitpack_compare_sweep": 232, + "bitpack_compare_sweep": 56, "bitpacking_filter": 0, "bitpacking_take": 40, - "canonicalize_bench": 32, + "canonicalize_bench": 14, "cast_bitpacked": 6, - "compute_between": 33, + "compute_between": 11, "for_decode": 0, "for_encode": 0 }, @@ -123,7 +123,7 @@ }, "vortex-fsst": { "chunked_dict_fsst_builder": 12, - "fsst_compress": 66, + "fsst_compress": 22, "fsst_like": 14, "fsst_take": 24, "fsst_url_compare": 8 @@ -147,7 +147,7 @@ "row_encode": 5 }, "vortex-sparse": { - "sparse_canonical": 4, + "sparse_canonical": 6, "sparse_pushdown": 5 }, "vortex-spatial": { diff --git a/scripts/codspeed_bench_durations.json b/scripts/codspeed_bench_durations.json index 9a597a3d746..45ce217a5ae 100644 --- a/scripts/codspeed_bench_durations.json +++ b/scripts/codspeed_bench_durations.json @@ -1,173 +1,173 @@ { - "source": "https://github.com/vortex-data/vortex/actions/runs/41711616261", - "commit": "24cd0eac8f87c02e990375b30bab3ca4bbf72284", + "source": "https://github.com/vortex-data/vortex/actions/runs/41869576169", + "commit": "141abdc590aa990c5e7b6c49dbc6832c254d50d4", "seconds": { "vortex": { - "common_encoding_tree_throughput": 10.125, - "pipeline": 0.287, - "single_encoding_throughput": 11.357, - "take_union": 8.234 + "common_encoding_tree_throughput": 4.073, + "pipeline": 0.253, + "single_encoding_throughput": 5.819, + "take_union": 3.473 }, "vortex-alp": { - "alp_compress": 9.783 + "alp_compress": 5.189 }, "vortex-array": { - "aggregate_grouped": 8.464, - "aggregate_max": 8.12, - "aggregate_sum": 7.173, - "binary_ops": 7.678, - "bool_zip": 2.508, - "cast_decimal": 2.755, - "cast_primitive": 7.48, - "chunk_array_builder": 9.008, - "chunked_dict_builder": 3.757, - "chunked_fsl_canonicalize": 7.35, - "compare": 7.588, - "compare_sorted": 7.584, - "copy_to_canonical_width": 0.293, - "dict_compare": 4.628, - "dict_compress": 4.48, - "dict_mask": 4.735, - "dict_unreferenced_mask": 2.664, - "expr_case_when": 5.711, - "expr_large_struct_pack": 7.823, - "expr_optimize": 7.023, - "filter_bool": 4.714, - "filter_fixed_width": 3.687, - "filter_fsl": 2.558, - "filter_list": 3.578, - "interleave": 3.308, - "kleene_bool": 8.215, - "like": 8.466, - "list_contains_set": 7.751, - "list_length": 7.71, - "list_sum": 9.001, - "listview_builder_extend": 7.729, - "listview_rebuild": 8.04, - "listview_zip": 7.503, - "patches_lookup": 2.696, - "piecewise_sequence_take_primitive": 3.237, - "primitive_zip": 7.668, - "probe": 2.661, - "row_fn_bool_retry": 0.42, - "row_fn_output": 0.42, - "row_fn_utf8_input": 0.418, - "run_end_compress": 9.424, - "run_end_decode": 8.016, - "run_end_decode_primitive": 7.753, - "run_end_filter": 7.925, - "run_end_null_count": 8.231, - "run_end_sum": 2.583, - "run_end_take": 2.796, - "scalar_at_struct": 2.476, - "scalar_construction": 7.346, - "scalar_fn_probe": 7.764, - "scalar_subtract": 0.417, - "search_sorted": 1.13, - "slice_dict_primitive": 7.284, - "take_chunked": 0.321, - "take_filter": 5.251, - "take_fsl": 3.654, - "take_patches": 3.692, - "take_primitive": 3.337, - "take_struct": 2.555, - "take_varbin": 2.399, - "validity_is_valid": 7.238, - "varbinview_compact": 7.362, - "varbinview_fill_null": 7.144, - "varbinview_try_new": 2.679, - "varbinview_zip": 2.526, - "widen": 10.159, - "widen_decimal": 7.503 + "aggregate_grouped": 3.823, + "aggregate_max": 3.586, + "aggregate_sum": 3.199, + "binary_ops": 2.881, + "bool_zip": 2.524, + "cast_decimal": 2.634, + "cast_primitive": 2.705, + "chunk_array_builder": 4.669, + "chunked_dict_builder": 3.92, + "chunked_fsl_canonicalize": 2.85, + "compare": 2.887, + "compare_sorted": 2.552, + "copy_to_canonical_width": 0.264, + "dict_compare": 4.881, + "dict_compress": 4.688, + "dict_mask": 4.854, + "dict_unreferenced_mask": 2.816, + "expr_case_when": 5.956, + "expr_large_struct_pack": 2.408, + "expr_optimize": 2.377, + "filter_bool": 5.08, + "filter_fixed_width": 3.863, + "filter_fsl": 2.687, + "filter_list": 3.708, + "interleave": 3.356, + "kleene_bool": 3.171, + "like": 3.308, + "list_contains_set": 3.038, + "list_length": 3.084, + "list_sum": 3.835, + "listview_builder_extend": 2.756, + "listview_rebuild": 3.062, + "listview_zip": 2.745, + "patches_lookup": 2.73, + "piecewise_sequence_take_primitive": 2.998, + "primitive_zip": 2.671, + "probe": 2.779, + "row_fn_bool_retry": 0.325, + "row_fn_output": 0.329, + "row_fn_utf8_input": 0.321, + "run_end_compress": 4.505, + "run_end_decode": 3.32, + "run_end_decode_primitive": 2.917, + "run_end_filter": 2.833, + "run_end_null_count": 3.881, + "run_end_sum": 2.468, + "run_end_take": 2.804, + "scalar_at_struct": 2.614, + "scalar_construction": 2.492, + "scalar_fn_probe": 2.918, + "scalar_subtract": 0.328, + "search_sorted": 0.543, + "slice_dict_primitive": 2.451, + "take_chunked": 0.33, + "take_filter": 5.694, + "take_fsl": 3.857, + "take_patches": 3.807, + "take_primitive": 3.558, + "take_struct": 2.642, + "take_varbin": 2.5, + "validity_is_valid": 2.515, + "varbinview_compact": 2.637, + "varbinview_fill_null": 2.419, + "varbinview_try_new": 2.675, + "varbinview_zip": 2.461, + "widen": 3.774, + "widen_decimal": 2.824 }, "vortex-buffer": { - "allocation": 2.877, - "collect_bool": 2.377, - "cpu_dispatch": 1.269, - "take_bits": 0.297, - "vortex_bitbuffer": 3.676, - "vortex_buffer": 2.864 + "allocation": 1.24, + "collect_bool": 0.727, + "cpu_dispatch": 0.564, + "take_bits": 0.265, + "vortex_bitbuffer": 1.413, + "vortex_buffer": 1.031 }, "vortex-compressor": { - "dict_encode": 0.425, - "float_stats": 7.996, - "integer_stats": 7.899, - "stats_calc": 0.285 + "dict_encode": 0.334, + "float_stats": 2.797, + "integer_stats": 2.828, + "stats_calc": 0.259 }, "vortex-compute": { - "lane_kernels": 6.908 + "lane_kernels": 1.034 }, "vortex-datetime-parts": { - "split_temporal": 7.41 + "split_temporal": 2.741 }, "vortex-decimal-byte-parts": { - "dbp_assemble": 0.297, - "dbp_compute": 9.177, - "dbp_split": 0.29 + "dbp_assemble": 0.259, + "dbp_compute": 4.161, + "dbp_split": 0.259 }, "vortex-elias-fano": { - "unary": 8.611 + "unary": 3.413 }, "vortex-fastlanes": { - "bitpack_blocked": 0.362, - "bitpack_blocked_decompress": 0.362, - "bitpack_compare": 3.001, - "bitpack_compare_sweep": 6.446, - "bitpacking_filter": 0.363, - "bitpacking_take": 7.35, - "canonicalize_bench": 4.076, - "cast_bitpacked": 3.27, - "compute_between": 3.628, - "for_decode": 0.361, - "for_encode": 0.357 + "bitpack_blocked": 0.367, + "bitpack_blocked_decompress": 0.369, + "bitpack_compare": 2.911, + "bitpack_compare_sweep": 6.813, + "bitpacking_filter": 0.368, + "bitpacking_take": 6.285, + "canonicalize_bench": 4.221, + "cast_bitpacked": 3.256, + "compute_between": 3.693, + "for_decode": 0.366, + "for_encode": 0.368 }, "vortex-file": { - "split_collection": 7.166 + "split_collection": 7.372 }, "vortex-fsst": { - "chunked_dict_fsst_builder": 8.917, - "fsst_compress": 9.615, - "fsst_like": 8.386, - "fsst_take": 12.768, - "fsst_url_compare": 8.272 + "chunked_dict_fsst_builder": 4.181, + "fsst_compress": 4.946, + "fsst_like": 3.697, + "fsst_take": 8.289, + "fsst_url_compare": 3.405 }, "vortex-layout": { - "aggregate_partial_merge": 5.62, - "bloom_filter": 4.933, - "pipeline_scan": 7.054, - "zone_map_prune": 6.858 + "aggregate_partial_merge": 5.896, + "bloom_filter": 5.447, + "pipeline_scan": 6.542, + "zone_map_prune": 7.638 }, "vortex-mask": { - "intersect_by_rank": 2.554, - "mask_iteration": 2.287, - "rank": 2.157, - "valid_counts": 2.02 + "intersect_by_rank": 1.29, + "mask_iteration": 1.038, + "rank": 0.723, + "valid_counts": 0.756 }, "vortex-pco": { - "scalar": 7.955 + "scalar": 3.03 }, "vortex-row": { - "row_encode": 8.766 + "row_encode": 2.976 }, "vortex-sparse": { - "sparse_canonical": 7.894, - "sparse_pushdown": 7.672 + "sparse_canonical": 3.52, + "sparse_pushdown": 3.121 }, "vortex-spatial": { - "area": 13.595, - "binary_predicates": 9.239, - "collect": 5.069, - "convex_hull": 5.064, - "distance": 6.949, - "envelope": 5.535, - "length": 13.364, - "make_line": 12.959, - "predicate_bbox": 0.706 + "area": 5.079, + "binary_predicates": 9.029, + "collect": 5.074, + "convex_hull": 4.98, + "distance": 5.132, + "envelope": 4.944, + "length": 4.81, + "make_line": 4.601, + "predicate_bbox": 0.703 }, "vortex-tensor": { - "cosine_similarity": 12.459, - "inner_product": 11.783, - "l2_norm": 5.36 + "cosine_similarity": 4.48, + "inner_product": 4.206, + "l2_norm": 4.136 } } } From 8d9e02746cd5b750b644300506147dcf4da45426 Mon Sep 17 00:00:00 2001 From: Gunther Xing Date: Thu, 5 Nov 2026 01:17:29 +0800 Subject: [PATCH 28/41] feat(array): migrate simple encodings to scalar probes (#10859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Tracking Issue: #9971 `OperationsVTable::scalar_at` cannot retain preparation between row reads. The probe API introduced in #9843 retains encoding state, validity, and child probes across repeated reads. This PR migrates 8 implementations: Bool, Constant, Decimal, Null, VarBinView, ByteBool, Sequence, PythonVTable. - These implementations use `ProbeState = ()`; generic probe validity is reused where needed. - `Constant` and `Null` scalars supply their own nullness - `Sequence` computes values directly - `PythonVTable` retains its unsupported scalar-access behavior. - Each migrated `scalar_at` remains a one-off probe compatibility shim. ### Verification - `cargo nextest run --cargo-profile ci --locked -p vortex-array -p vortex-bytebool -p vortex-sequence --all-features --no-fail-fast` — 4,832 passed, 1 skipped; the Python-binding Rust suite also passed (33 tests). - `uv run --locked --all-packages --reinstall-package vortex-data pytest --benchmark-disable vortex-python/test/test_pyarray.py vortex-python/test/test_scalar.py vortex-python/test/test_array.py vortex-python/test/test_encodings.py` — 86 passed, 1 skipped. - `cargo test --profile ci --locked -p vortex-array -p vortex-bytebool -p vortex-sequence -p vortex-python --all-features --doc --no-fail-fast` — 85 passed, 21 ignored. - `cargo +nightly-2026-09-10 fmt -p vortex-array -p vortex-bytebool -p vortex-sequence -p vortex-python --check` — passed. - `cargo clippy --profile ci --locked -p vortex-array -p vortex-bytebool -p vortex-sequence -p vortex-python --all-targets --all-features -- -D warnings` — passed. - Focused `cargo bench` runs — all 3 benchmark targets completed successfully. ## Comparison to baseline **Measured on a development machine (macOS with Apple Silicon); not representative of production performance** `cargo bench -p --bench -- --min-time 0.1 --sample-count 15`. Values are Divan medians in microseconds per iteration, with one retained probe per iteration. | Array | Fixture | Reads | Baseline (µs) | Migrated (µs) | Change | | --- | --- | ---: | ---: | ---: | ---: | | Bool | nullable random | 256 | 4.957 | 4.624 | -6.7% | | Constant | fixed scalar, random | 256 | 2.666 | 2.103 | -21.1% | | Decimal | decimal128, random | 256 | 4.541 | 3.957 | -12.9% | | Null | all null, random | 256 | 1.958 | 1.260 | -35.6% | | VarBinView | nullable strings, random | 256 | 6.290 | 5.707 | -9.3% | | ByteBool | nullable random | 256 | 14.080 | 4.957 | -64.8% | | Sequence | i64 arithmetic, random | 256 | 8.582 | 2.333 | -72.8% | | PythonVTable | Scalar probing is unsupported by this vtable | — | N/A | N/A | N/A | ## AI Usage GPT-6-Sol implemented the changes with Codex. The human reviewed all the code and can explain every line of it. Benchmark results and the origin/develop comparisons are done by Codex. --------- Signed-off-by: Gunther Xing --- Cargo.lock | 4 ++ encodings/bytebool/Cargo.toml | 6 ++ encodings/bytebool/benches/probe.rs | 60 +++++++++++++++++++ encodings/bytebool/src/array.rs | 19 +++++- encodings/sequence/Cargo.toml | 6 ++ encodings/sequence/benches/probe.rs | 55 +++++++++++++++++ encodings/sequence/src/array.rs | 14 ++++- vortex-array/benches/probe.rs | 6 +- .../src/arrays/bool/vtable/operations.rs | 19 +++++- .../src/arrays/constant/vtable/operations.rs | 15 ++++- .../src/arrays/decimal/vtable/operations.rs | 19 +++++- vortex-array/src/arrays/null/mod.rs | 13 +++- .../arrays/varbinview/vtable/operations.rs | 19 +++++- vortex-python/src/arrays/py/vtable.rs | 13 +++- 14 files changed, 246 insertions(+), 22 deletions(-) create mode 100644 encodings/bytebool/benches/probe.rs create mode 100644 encodings/sequence/benches/probe.rs diff --git a/Cargo.lock b/Cargo.lock index d2e081a74a6..4ee30a8b969 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11611,6 +11611,8 @@ dependencies = [ name = "vortex-bytebool" version = "0.1.0" dependencies = [ + "codspeed-divan-compat", + "mimalloc", "num-traits", "rstest", "vortex-array", @@ -12441,6 +12443,8 @@ dependencies = [ name = "vortex-sequence" version = "0.1.0" dependencies = [ + "codspeed-divan-compat", + "mimalloc", "num-traits", "prost 0.14.4", "rstest", diff --git a/encodings/bytebool/Cargo.toml b/encodings/bytebool/Cargo.toml index 3bbe4a85a54..fa656d6f031 100644 --- a/encodings/bytebool/Cargo.toml +++ b/encodings/bytebool/Cargo.toml @@ -25,5 +25,11 @@ vortex-mask = { workspace = true } vortex-session = { workspace = true } [dev-dependencies] +divan = { workspace = true } +mimalloc = { workspace = true } rstest = { workspace = true } vortex-array = { workspace = true, features = ["_test-harness"] } + +[[bench]] +name = "probe" +harness = false diff --git a/encodings/bytebool/benches/probe.rs b/encodings/bytebool/benches/probe.rs new file mode 100644 index 00000000000..4bbac22f87d --- /dev/null +++ b/encodings/bytebool/benches/probe.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#![expect(clippy::unwrap_used)] + +use std::sync::LazyLock; + +use divan::Bencher; +use mimalloc::MiMalloc; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_bytebool::ByteBool; +use vortex_session::VortexSession; + +static SESSION: LazyLock = LazyLock::new(VortexSession::empty); + +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +const LEN: usize = 65_536; +const ACCESSES: usize = 256; + +fn main() { + LazyLock::force(&SESSION); + divan::main(); +} + +#[divan::bench] +fn one_off(bencher: Bencher) { + let array = ByteBool::from_option_vec( + (0..LEN) + .map(|index| (index % 7 != 0).then_some(index % 2 == 0)) + .collect(), + ) + .into_array(); + bencher + .with_inputs(|| SESSION.create_execution_ctx()) + .bench_refs(|ctx| { + for read in 0..ACCESSES { + divan::black_box(array.execute_scalar((read * 7919) % LEN, ctx).unwrap()); + } + }); +} + +#[divan::bench] +fn repeated(bencher: Bencher) { + let array = ByteBool::from_option_vec( + (0..LEN) + .map(|index| (index % 7 != 0).then_some(index % 2 == 0)) + .collect(), + ) + .into_array(); + bencher + .with_inputs(|| (SESSION.create_execution_ctx(), array.repeated_probe())) + .bench_refs(|(ctx, probe)| { + for read in 0..ACCESSES { + divan::black_box(probe.execute_scalar((read * 7919) % LEN, ctx).unwrap()); + } + }); +} diff --git a/encodings/bytebool/src/array.rs b/encodings/bytebool/src/array.rs index 9413cbaa8b4..88007ecfd01 100644 --- a/encodings/bytebool/src/array.rs +++ b/encodings/bytebool/src/array.rs @@ -18,6 +18,7 @@ use vortex_array::EqMode; use vortex_array::ExecutionCtx; use vortex_array::ExecutionResult; use vortex_array::IntoArray; +use vortex_array::ProbeState; use vortex_array::TypedArrayRef; use vortex_array::array_slots; use vortex_array::arrays::BoolArray; @@ -308,16 +309,28 @@ impl ValidityVTable for ByteBool { impl OperationsVTable for ByteBool { type ProbeState = (); - fn scalar_at( - array: ArrayView<'_, ByteBool>, + fn probe_scalar( + state: &mut ProbeState<'_, ByteBool>, index: usize, - _ctx: &mut ExecutionCtx, + ctx: &mut ExecutionCtx, ) -> VortexResult { + let array = state.array(); + if !state.is_valid(index, ctx)? { + return Ok(Scalar::null(array.dtype().clone())); + } Ok(Scalar::bool( array.buffer.as_host()[index] == 1, array.dtype().nullability(), )) } + + fn scalar_at( + array: ArrayView<'_, ByteBool>, + index: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + Self::probe_scalar(&mut ProbeState::once(array), index, ctx) + } } #[cfg(test)] diff --git a/encodings/sequence/Cargo.toml b/encodings/sequence/Cargo.toml index a6233c27a7b..bebe4db6cf2 100644 --- a/encodings/sequence/Cargo.toml +++ b/encodings/sequence/Cargo.toml @@ -24,8 +24,14 @@ vortex-mask = { workspace = true } vortex-session = { workspace = true } [dev-dependencies] +divan = { workspace = true } +mimalloc = { workspace = true } rstest = { workspace = true } vortex-array = { path = "../../vortex-array", features = ["_test-harness"] } +[[bench]] +name = "probe" +harness = false + [lints] workspace = true diff --git a/encodings/sequence/benches/probe.rs b/encodings/sequence/benches/probe.rs new file mode 100644 index 00000000000..4d42fbfea27 --- /dev/null +++ b/encodings/sequence/benches/probe.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#![expect(clippy::unwrap_used)] + +use std::sync::LazyLock; + +use divan::Bencher; +use mimalloc::MiMalloc; +use vortex_array::IntoArray; +use vortex_array::VortexSessionExecute; +use vortex_array::dtype::Nullability; +use vortex_sequence::Sequence; +use vortex_session::VortexSession; + +static SESSION: LazyLock = LazyLock::new(VortexSession::empty); + +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +const LEN: usize = 65_536; +const ACCESSES: usize = 256; + +fn main() { + LazyLock::force(&SESSION); + divan::main(); +} + +#[divan::bench] +fn one_off(bencher: Bencher) { + let array = Sequence::try_new_typed(42i64, 3i64, Nullability::NonNullable, LEN) + .unwrap() + .into_array(); + bencher + .with_inputs(|| SESSION.create_execution_ctx()) + .bench_refs(|ctx| { + for read in 0..ACCESSES { + divan::black_box(array.execute_scalar((read * 7919) % LEN, ctx).unwrap()); + } + }); +} + +#[divan::bench] +fn repeated(bencher: Bencher) { + let array = Sequence::try_new_typed(42i64, 3i64, Nullability::NonNullable, LEN) + .unwrap() + .into_array(); + bencher + .with_inputs(|| (SESSION.create_execution_ctx(), array.repeated_probe())) + .bench_refs(|(ctx, probe)| { + for read in 0..ACCESSES { + divan::black_box(probe.execute_scalar((read * 7919) % LEN, ctx).unwrap()); + } + }); +} diff --git a/encodings/sequence/src/array.rs b/encodings/sequence/src/array.rs index 09bad37644a..22438e35494 100644 --- a/encodings/sequence/src/array.rs +++ b/encodings/sequence/src/array.rs @@ -20,6 +20,7 @@ use vortex_array::ArrayView; use vortex_array::EqMode; use vortex_array::ExecutionCtx; use vortex_array::ExecutionResult; +use vortex_array::ProbeState; use vortex_array::buffer::BufferHandle; use vortex_array::dtype::DType; use vortex_array::dtype::NativePType; @@ -436,16 +437,25 @@ impl VTable for Sequence { impl OperationsVTable for Sequence { type ProbeState = (); - fn scalar_at( - array: ArrayView<'_, Sequence>, + fn probe_scalar( + state: &mut ProbeState<'_, Sequence>, index: usize, _ctx: &mut ExecutionCtx, ) -> VortexResult { + let array = state.array(); Scalar::try_new( array.dtype().clone(), Some(ScalarValue::Primitive(array.index_value(index))), ) } + + fn scalar_at( + array: ArrayView<'_, Sequence>, + index: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + Self::probe_scalar(&mut ProbeState::once(array), index, ctx) + } } impl ValidityVTable for Sequence { diff --git a/vortex-array/benches/probe.rs b/vortex-array/benches/probe.rs index aaf15a362d5..d1947c43abe 100644 --- a/vortex-array/benches/probe.rs +++ b/vortex-array/benches/probe.rs @@ -12,6 +12,7 @@ use mimalloc::MiMalloc; use vortex_array::ArrayRef; use vortex_array::IntoArray; use vortex_array::VortexSessionExecute; +use vortex_array::arrays::BoolArray; use vortex_array::arrays::DictArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::StructArray; @@ -80,13 +81,16 @@ fn indices() -> Vec { .collect() } -const ARRAYS: &[&str] = &["primitive", "dict", "struct"]; +const ARRAYS: &[&str] = &["primitive", "dict", "struct", "bool"]; fn array(name: &str) -> ArrayRef { match name { "primitive" => nullable_primitive(LEN), "dict" => dict_over_nullable_values(), "struct" => nullable_struct(), + "bool" => { + BoolArray::from_iter((0..LEN).map(|i| (i % 7 != 0).then_some(i % 2 == 0))).into_array() + } _ => unreachable!("unknown fixture {name}"), } } diff --git a/vortex-array/src/arrays/bool/vtable/operations.rs b/vortex-array/src/arrays/bool/vtable/operations.rs index 46267b96317..e92c0e662fc 100644 --- a/vortex-array/src/arrays/bool/vtable/operations.rs +++ b/vortex-array/src/arrays/bool/vtable/operations.rs @@ -6,6 +6,7 @@ use vortex_error::VortexResult; use crate::ExecutionCtx; use crate::array::ArrayView; use crate::array::OperationsVTable; +use crate::array::ProbeState; use crate::arrays::Bool; use crate::arrays::bool::BoolArrayExt; use crate::scalar::Scalar; @@ -13,16 +14,28 @@ use crate::scalar::Scalar; impl OperationsVTable for Bool { type ProbeState = (); - fn scalar_at( - array: ArrayView<'_, Bool>, + fn probe_scalar( + state: &mut ProbeState<'_, Bool>, index: usize, - _ctx: &mut ExecutionCtx, + ctx: &mut ExecutionCtx, ) -> VortexResult { + let array = state.array(); + if !state.is_valid(index, ctx)? { + return Ok(Scalar::null(array.dtype().clone())); + } Ok(Scalar::bool( array.bit_buffer_view().value(index), array.dtype().nullability(), )) } + + fn scalar_at( + array: ArrayView<'_, Bool>, + index: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + Self::probe_scalar(&mut ProbeState::once(array), index, ctx) + } } #[cfg(test)] diff --git a/vortex-array/src/arrays/constant/vtable/operations.rs b/vortex-array/src/arrays/constant/vtable/operations.rs index a26187d080b..1a5ad381fb6 100644 --- a/vortex-array/src/arrays/constant/vtable/operations.rs +++ b/vortex-array/src/arrays/constant/vtable/operations.rs @@ -6,18 +6,27 @@ use vortex_error::VortexResult; use crate::ExecutionCtx; use crate::array::ArrayView; use crate::array::OperationsVTable; +use crate::array::ProbeState; use crate::arrays::Constant; use crate::scalar::Scalar; impl OperationsVTable for Constant { type ProbeState = (); - fn scalar_at( - array: ArrayView<'_, Constant>, + fn probe_scalar( + state: &mut ProbeState<'_, Constant>, _index: usize, _ctx: &mut ExecutionCtx, ) -> VortexResult { - Ok(array.scalar.clone()) + Ok(state.array().scalar.clone()) + } + + fn scalar_at( + array: ArrayView<'_, Constant>, + index: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + Self::probe_scalar(&mut ProbeState::once(array), index, ctx) } } diff --git a/vortex-array/src/arrays/decimal/vtable/operations.rs b/vortex-array/src/arrays/decimal/vtable/operations.rs index c43ff65c7a6..19b80cd103a 100644 --- a/vortex-array/src/arrays/decimal/vtable/operations.rs +++ b/vortex-array/src/arrays/decimal/vtable/operations.rs @@ -6,6 +6,7 @@ use vortex_error::VortexResult; use crate::ExecutionCtx; use crate::array::ArrayView; use crate::array::OperationsVTable; +use crate::array::ProbeState; use crate::arrays::Decimal; use crate::match_each_decimal_value_type; use crate::scalar::DecimalValue; @@ -14,11 +15,15 @@ use crate::scalar::Scalar; impl OperationsVTable for Decimal { type ProbeState = (); - fn scalar_at( - array: ArrayView<'_, Decimal>, + fn probe_scalar( + state: &mut ProbeState<'_, Decimal>, index: usize, - _ctx: &mut ExecutionCtx, + ctx: &mut ExecutionCtx, ) -> VortexResult { + let array = state.array(); + if !state.is_valid(index, ctx)? { + return Ok(Scalar::null(array.dtype().clone())); + } // Stored values can fall outside the precision, see `DecimalData::new_unchecked`. match_each_decimal_value_type!(array.values_type(), |D| { Scalar::try_decimal( @@ -28,6 +33,14 @@ impl OperationsVTable for Decimal { ) }) } + + fn scalar_at( + array: ArrayView<'_, Decimal>, + index: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + Self::probe_scalar(&mut ProbeState::once(array), index, ctx) + } } #[cfg(test)] diff --git a/vortex-array/src/arrays/null/mod.rs b/vortex-array/src/arrays/null/mod.rs index 6c0160c4e15..e55212db65a 100644 --- a/vortex-array/src/arrays/null/mod.rs +++ b/vortex-array/src/arrays/null/mod.rs @@ -20,6 +20,7 @@ use crate::array::ArrayParts; use crate::array::ArrayView; use crate::array::EmptyArrayData; use crate::array::OperationsVTable; +use crate::array::ProbeState; use crate::array::VTable; use crate::array::ValidityVTable; use crate::array::with_empty_buffers; @@ -197,13 +198,21 @@ impl Array { impl OperationsVTable for Null { type ProbeState = (); - fn scalar_at( - _array: ArrayView<'_, Null>, + fn probe_scalar( + _state: &mut ProbeState<'_, Null>, _index: usize, _ctx: &mut ExecutionCtx, ) -> VortexResult { Ok(Scalar::null(DType::Null)) } + + fn scalar_at( + array: ArrayView<'_, Null>, + index: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + Self::probe_scalar(&mut ProbeState::once(array), index, ctx) + } } impl ValidityVTable for Null { diff --git a/vortex-array/src/arrays/varbinview/vtable/operations.rs b/vortex-array/src/arrays/varbinview/vtable/operations.rs index a06cf8142bc..71f71f76892 100644 --- a/vortex-array/src/arrays/varbinview/vtable/operations.rs +++ b/vortex-array/src/arrays/varbinview/vtable/operations.rs @@ -6,6 +6,7 @@ use vortex_error::VortexResult; use crate::ExecutionCtx; use crate::array::ArrayView; use crate::array::OperationsVTable; +use crate::array::ProbeState; use crate::arrays::VarBinView; use crate::arrays::varbin::varbin_scalar_unchecked; use crate::scalar::Scalar; @@ -13,13 +14,25 @@ use crate::scalar::Scalar; impl OperationsVTable for VarBinView { type ProbeState = (); - fn scalar_at( - array: ArrayView<'_, VarBinView>, + fn probe_scalar( + state: &mut ProbeState<'_, VarBinView>, index: usize, - _ctx: &mut ExecutionCtx, + ctx: &mut ExecutionCtx, ) -> VortexResult { + let array = state.array(); + if !state.is_valid(index, ctx)? { + return Ok(Scalar::null(array.dtype().clone())); + } // SAFETY: `VarBinViewArray` has a Utf8 or Binary dtype, and it checks that all Utf8 data // is valid UTF-8. Ok(unsafe { varbin_scalar_unchecked(array.bytes_at(index), array.dtype()) }) } + + fn scalar_at( + array: ArrayView<'_, VarBinView>, + index: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + Self::probe_scalar(&mut ProbeState::once(array), index, ctx) + } } diff --git a/vortex-python/src/arrays/py/vtable.rs b/vortex-python/src/arrays/py/vtable.rs index 686390fdddf..333a4b0f1a9 100644 --- a/vortex-python/src/arrays/py/vtable.rs +++ b/vortex-python/src/arrays/py/vtable.rs @@ -15,6 +15,7 @@ use vortex::array::EqMode; use vortex::array::ExecutionCtx; use vortex::array::ExecutionResult; use vortex::array::OperationsVTable; +use vortex::array::ProbeState; use vortex::array::VTable; use vortex::array::ValidityVTable; use vortex::array::buffer::BufferHandle; @@ -124,13 +125,21 @@ impl VTable for PythonVTable { impl OperationsVTable for PythonVTable { type ProbeState = (); - fn scalar_at( - _array: ArrayView<'_, PythonVTable>, + fn probe_scalar( + _state: &mut ProbeState<'_, PythonVTable>, _index: usize, _ctx: &mut ExecutionCtx, ) -> VortexResult { vortex_bail!("PythonArray scalar_at is not supported"); } + + fn scalar_at( + array: ArrayView<'_, PythonVTable>, + index: usize, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + Self::probe_scalar(&mut ProbeState::once(array), index, ctx) + } } impl ValidityVTable for PythonVTable { From e72a61dfcd382e81b0c5c4872d1fed1c27122d43 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 12:44:01 -0500 Subject: [PATCH 29/41] Keep same-scale decimal widening casts lazy (#11006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracking Issue: #10701 Top of the decimal `Widen` stack, on #11005. No release gate. ## Summary A same-scale precision increase copied every value into a wider buffer even when the caller did not need the values yet. Returning a `Widen` defers that copy and lets subsequent kernels keep working on the narrow child. Basically, we might as well keep things in `Widen` form instead of casting immediately. ## Changes The child retains the source dtype and statistics because its values are unchanged. A cast of a decimal `Widen` passes to that child. Execution copies into the logical width directly, since yielding another widening cast would return a `Widen` again. The default-writer test compares round-trip values. Known cost: the retained benchmarks show slower filtering at precision 38 (see #10946).
Historical local benchmark results These retained measurements predate this style pass and rebase. They do not measure the current head. `cargo bench -p vortex-array --bench widen_decimal`, Apple M4, medians. The numbers were measured at the top of this stack before the split, so they cover the whole stack, not this layer alone. The baseline is the canonical input, and the candidate is the same values as a `Widen` (child precision 5 under precision 10, 9 under 18, and 18 under 38). CodSpeed on this PR measures the layer itself. | bench | p10 canonical | p10 Widen | p18 canonical | p18 Widen | p38 canonical | p38 Widen | | --- | ---: | ---: | ---: | ---: | ---: | ---: | | `cast_widen` | 12.08 µs | 6.958 µs | 12.33 µs | 6.958 µs | 16.54 µs | 14.58 µs |
## API Changes A same-scale decimal cast returns a `Widen` when the target precision grows the canonical width. --------- Signed-off-by: Connor Tsui --- scripts/codspeed_bench_counts.json | 2 +- vortex-array/benches/widen_decimal.rs | 44 +++ .../src/arrays/decimal/compute/cast.rs | 106 ++---- vortex-array/src/arrays/widen/compute/cast.rs | 79 +++- vortex-array/src/arrays/widen/mod.rs | 7 +- .../src/arrays/widen/tests/decimal_cast.rs | 350 ++++++++++++++++++ .../src/arrays/widen/tests/decimal_compare.rs | 2 + vortex-array/src/arrays/widen/tests/mod.rs | 1 + vortex-file/src/tests.rs | 30 ++ 9 files changed, 534 insertions(+), 87 deletions(-) create mode 100644 vortex-array/src/arrays/widen/tests/decimal_cast.rs diff --git a/scripts/codspeed_bench_counts.json b/scripts/codspeed_bench_counts.json index 32222ba7fa8..ea2d0db2a8e 100644 --- a/scripts/codspeed_bench_counts.json +++ b/scripts/codspeed_bench_counts.json @@ -75,7 +75,7 @@ "varbinview_try_new": 6, "varbinview_zip": 2, "widen": 17, - "widen_decimal": 6 + "widen_decimal": 9 }, "vortex-buffer": { "allocation": 35, diff --git a/vortex-array/benches/widen_decimal.rs b/vortex-array/benches/widen_decimal.rs index 8ccb87094da..036664926f4 100644 --- a/vortex-array/benches/widen_decimal.rs +++ b/vortex-array/benches/widen_decimal.rs @@ -8,6 +8,9 @@ //! Canonical execution is omitted because it only returns the existing array. Each iteration shares //! value buffers and builds fresh arrays so cached statistics cannot remove the operation's work. //! [`WidenArray::encode_decimal`] selects the child buffers outside the timed region. +//! +//! Casts from precision 38 to 76 measure lazy construction for canonical and `Widen` inputs. +//! A canonical-input cast also measures execution through the copy into `i256` values. use std::fmt; use std::sync::LazyLock; @@ -28,6 +31,7 @@ use vortex_array::arrays::DecimalArray; use vortex_array::arrays::Widen; use vortex_array::arrays::WidenArray; use vortex_array::arrays::widen::WidenArraySlotsExt; +use vortex_array::builtins::ArrayBuiltins; use vortex_array::dtype::DType; use vortex_array::dtype::DecimalDType; use vortex_array::dtype::DecimalType; @@ -124,6 +128,13 @@ fn widen_cases() -> Vec { .collect() } +fn cast_cases() -> Vec { + cases() + .into_iter() + .filter(|case| case.precision == 38) + .collect() +} + impl fmt::Display for Case { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( @@ -260,3 +271,36 @@ fn execute(bencher: Bencher, case: Case) { |array, ctx| execute_canonical(Ok(array), ctx), ); } + +#[divan::bench(args = cast_cases())] +fn cast_lazy(bencher: Bencher, case: Case) { + let values = case.values(); + let target = DType::Decimal(DecimalDType::new(76, SCALE), Nullability::NonNullable); + + bench_case( + bencher, + || values.array(case), + |array, _ctx| { + array + .cast(target.clone()) + .vortex_expect("a same-scale precision increase is valid") + }, + ); +} + +#[divan::bench] +fn cast_execute(bencher: Bencher) { + let case = Case { + precision: 38, + child_precision: 18, + input: Input::Canonical, + }; + let values = case.values(); + let target = DType::Decimal(DecimalDType::new(76, SCALE), Nullability::NonNullable); + + bench_case( + bencher, + || values.array(case), + |array, ctx| execute_canonical(array.cast(target.clone()), ctx), + ); +} diff --git a/vortex-array/src/arrays/decimal/compute/cast.rs b/vortex-array/src/arrays/decimal/compute/cast.rs index c8f561af0b7..fd55b107a6f 100644 --- a/vortex-array/src/arrays/decimal/compute/cast.rs +++ b/vortex-array/src/arrays/decimal/compute/cast.rs @@ -1,6 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +//! Decimal casts with checked precision, scale, and nullability. +//! +//! Same-scale casts that increase the canonical width defer the copy through a [`WidenArray`]. +//! Scale changes and precision reductions run the conversion kernel to check or rescale valid values. + use num_traits::CheckedMul; use vortex_buffer::Buffer; use vortex_buffer::BufferMut; @@ -21,6 +26,7 @@ use crate::arrays::ConstantArray; use crate::arrays::Decimal; use crate::arrays::DecimalArray; use crate::arrays::PrimitiveArray; +use crate::arrays::WidenArray; use crate::arrays::decimal::DecimalArrayExt; use crate::dtype::BigCast; use crate::dtype::DType; @@ -44,8 +50,6 @@ use crate::validity::Validity; impl CastReduce for Decimal { fn cast(array: ArrayView<'_, Decimal>, dtype: &DType) -> VortexResult> { - // Only nullability changes within the same decimal dtype are reducible without execution. - // Precision/scale changes need the kernel. let DType::Decimal(to_decimal_dtype, to_nullability) = dtype else { return Ok(None); }; @@ -56,7 +60,8 @@ impl CastReduce for Decimal { ); }; - if from_decimal_dtype != to_decimal_dtype { + let grows_width = grows_canonical_width(*from_decimal_dtype, *to_decimal_dtype); + if from_decimal_dtype != to_decimal_dtype && !grows_width { return Ok(None); } @@ -67,6 +72,10 @@ impl CastReduce for Decimal { return Ok(None); }; + if grows_width { + return widen_decimal_values(array, dtype, new_validity).map(Some); + } + // SAFETY: validity has the same length, only its nullability tag changes. unsafe { Ok(Some( @@ -121,6 +130,10 @@ impl CastKernel for Decimal { .clone() .cast_nullability(*to_nullability, array.len(), ctx)?; + if grows_canonical_width(*from_decimal_dtype, *to_decimal_dtype) { + return widen_decimal_values(array, dtype, new_validity).map(Some); + } + // Reuse the values buffer untouched when no rescale is required, the target precision // only widens (so every value still fits), and the current physical type is already wide // enough to hold the target precision. This keeps the common precision-widening cast @@ -169,6 +182,31 @@ impl CastKernel for Decimal { } } +/// Returns whether a same-scale cast increases the canonical width. +fn grows_canonical_width(from: DecimalDType, to: DecimalDType) -> bool { + from.scale() == to.scale() && from.canonical_values_type() < to.canonical_values_type() +} + +/// Wraps the source values in a [`WidenArray`] with the target dtype. +/// +/// The caller must cast `validity` to the target nullability without changing its length or valid +/// rows. The child keeps the source decimal dtype and statistics because its values are unchanged. +fn widen_decimal_values( + array: ArrayView<'_, Decimal>, + dtype: &DType, + validity: Validity, +) -> VortexResult { + let values = DecimalArray::new_handle( + array.buffer_handle().clone(), + array.values_type(), + array.decimal_dtype(), + validity, + ); + values.statistics().inherit_from(array.statistics()); + + Ok(WidenArray::try_new(values.into_array(), dtype.clone())?.into_array()) +} + fn cast_to_integer( array: ArrayView<'_, Decimal>, nullability: Nullability, @@ -966,68 +1004,6 @@ mod tests { assert_eq!(casted.buffer::().as_ref()[0], 1); } - #[test] - fn cast_upcast_precision_succeeds() { - let mut ctx = SESSION.create_execution_ctx(); - let array = DecimalArray::new( - buffer![100i32, 200, 300], - DecimalDType::new(10, 2), - Validity::NonNullable, - ); - - // Cast to higher precision with same scale - should succeed - let wider_dtype = DType::Decimal(DecimalDType::new(38, 2), Nullability::NonNullable); - let casted = array - .into_array() - .cast(wider_dtype) - .unwrap() - .execute::(&mut ctx) - .unwrap(); - - assert_eq!(casted.precision(), 38); - assert_eq!(casted.scale(), 2); - assert_eq!(casted.len(), 3); - // Should be stored in i128 now (precision 38 requires i128) - assert_eq!(casted.values_type(), DecimalType::I128); - assert_arrays_eq!( - casted, - DecimalArray::from_iter([100i128, 200, 300], DecimalDType::new(38, 2)), - &mut ctx - ); - } - - #[test] - fn cast_widening_same_physical_type_is_zero_copy() { - let mut ctx = SESSION.create_execution_ctx(); - // Decimal(10,2) and Decimal(18,2) are both physically i64 with the same scale, so widening - // the precision must reuse the values buffer rather than allocate and re-scan it. - let array = DecimalArray::new( - buffer![100i64, 200, 300], - DecimalDType::new(10, 2), - Validity::NonNullable, - ); - let src_ptr = array.buffer::().as_ptr(); - - let wider_dtype = DType::Decimal(DecimalDType::new(18, 2), Nullability::NonNullable); - let casted = array - .into_array() - .cast(wider_dtype) - .unwrap() - .execute::(&mut ctx) - .unwrap(); - - assert_eq!(casted.precision(), 18); - assert_eq!(casted.scale(), 2); - assert_eq!(casted.values_type(), DecimalType::I64); - assert_eq!(casted.buffer::().as_ref(), &[100, 200, 300]); - // The values buffer must be shared with the source (zero-copy), not reallocated. - assert_eq!( - casted.buffer::().as_ptr(), - src_ptr, - "precision-widening cast must reuse the source values buffer" - ); - } - #[test] fn cast_to_non_decimal_returns_err() { let mut ctx = SESSION.create_execution_ctx(); diff --git a/vortex-array/src/arrays/widen/compute/cast.rs b/vortex-array/src/arrays/widen/compute/cast.rs index 6f7131b2f90..940c57c2bf5 100644 --- a/vortex-array/src/arrays/widen/compute/cast.rs +++ b/vortex-array/src/arrays/widen/compute/cast.rs @@ -1,6 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +//! Cast reductions for decimal and primitive [`Widen`] arrays. +//! +//! Same-scale decimal casts retarget the `Widen` when the child already fits the target. Primitive +//! casts can retain a `Widen` for wider integer targets, after matching the child's signedness to the +//! target. Both paths leave required conversions and nullability checks to the child's cast. + use vortex_error::VortexResult; use crate::ArrayRef; @@ -10,33 +16,70 @@ use crate::arrays::widen::WidenArraySlotsExt; use crate::arrays::widen::compute::wrap_values; use crate::builtins::ArrayBuiltins; use crate::dtype::DType; +use crate::dtype::DecimalDType; use crate::dtype::PType; use crate::scalar_fn::fns::cast::CastReduce; impl CastReduce for Widen { fn cast(array: ArrayView<'_, Self>, dtype: &DType) -> VortexResult> { let values = array.values(); - let (DType::Primitive(target_ptype, nullability), DType::Primitive(values_ptype, _)) = - (dtype, values.dtype()) - else { - return Ok(None); - }; - - let cast_ptype = match values_ptype_for_cast(*values_ptype, *target_ptype) { - Some(cast_ptype) if target_ptype.byte_width() > cast_ptype.byte_width() => cast_ptype, - // A float target, or an integer target no wider than `cast_ptype`, goes to the values, - // whose cast checks every value and every null. - _ => return values.cast(dtype.clone()).map(Some), - }; - - // A wider integer target holds every value of `cast_ptype`, so the values stay narrow. - // They take the target nullability, so that a cast to a non-nullable target still fails on - // a null. - let values = values.cast(DType::Primitive(cast_ptype, *nullability))?; - wrap_values(values, dtype.clone()).map(Some) + + match values.dtype() { + DType::Decimal(values_dtype, _) => cast_decimal(values, *values_dtype, dtype), + DType::Primitive(values_ptype, _) => cast_primitive(values, *values_ptype, dtype), + _ => Ok(None), + } + } +} + +fn cast_decimal( + values: &ArrayRef, + values_dtype: DecimalDType, + dtype: &DType, +) -> VortexResult> { + if let DType::Decimal(target_dtype, _) = dtype + && values_dtype.scale() == target_dtype.scale() + && values_dtype.canonical_values_type() < target_dtype.canonical_values_type() + { + // The child's declared precision proves that every value fits the target. Only nullability + // may still need a check, so leave the width conversion to Widen execution. + let values = values.cast(values.dtype().with_nullability(dtype.nullability()))?; + + return wrap_values(values, dtype.clone()).map(Some); + } + + // Rescaling, narrower targets, and primitive conversions still need the child's cast. The child + // has the parent's values, scale, and nulls, so these conversions preserve the logical result. + match dtype { + DType::Decimal(..) | DType::Primitive(..) => values.cast(dtype.clone()).map(Some), + _ => Ok(None), } } +fn cast_primitive( + values: &ArrayRef, + values_ptype: PType, + dtype: &DType, +) -> VortexResult> { + let DType::Primitive(target_ptype, nullability) = dtype else { + return Ok(None); + }; + + let cast_ptype = match values_ptype_for_cast(values_ptype, *target_ptype) { + Some(cast_ptype) if target_ptype.byte_width() > cast_ptype.byte_width() => cast_ptype, + // A float needs value conversion, and an integer target no wider than the converted child + // cannot retain a Widen. Casting directly also checks overflow and nullability. + _ => return values.cast(dtype.clone()).map(Some), + }; + + // Match the child's signedness and nullability to the target. If both already match, the cast + // returns the original child. Otherwise, it converts the child and checks negative values and + // nulls as needed. Widen handles the remaining width increase because every child value fits. + let values = values.cast(DType::Primitive(cast_ptype, *nullability))?; + + wrap_values(values, dtype.clone()).map(Some) +} + /// Returns the type that the values take when the `Widen` is cast to the integer type `target`, or /// `None` for a float target. /// diff --git a/vortex-array/src/arrays/widen/mod.rs b/vortex-array/src/arrays/widen/mod.rs index 72ba52b208a..7d8284d2c26 100644 --- a/vortex-array/src/arrays/widen/mod.rs +++ b/vortex-array/src/arrays/widen/mod.rs @@ -11,10 +11,11 @@ //! [`WidenArray::encode`] picks the narrowest such type for a [`PrimitiveArray`]. //! //! A decimal child has the logical scale and a lower precision, and **must** be valid for its dtype. -//! Comparisons, bounds checks and fills use the child dtype to bound its valid values. +//! Comparisons, bounds checks, and fills use the child dtype to bound its valid values. //! [`WidenArray::encode_decimal`] and [`WidenArray::from_stored_decimal`] choose child precision -//! from valid values or supplied bounds, at the logical scale. Decimal execution copies the child -//! values into the logical canonical width. +//! from valid values or supplied bounds, at the logical scale. A widening decimal cast keeps the +//! source dtype on its child. Decimal execution copies child values directly into the logical +//! canonical width, since yielding a cast would produce another `Widen` instead of canonical values. //! //! Narrowing is a physical choice, so it **must not** change a result. Execution casts the values //! to the logical dtype for integers, and an operation runs on the values only where it gives the diff --git a/vortex-array/src/arrays/widen/tests/decimal_cast.rs b/vortex-array/src/arrays/widen/tests/decimal_cast.rs new file mode 100644 index 00000000000..e3dac36abbd --- /dev/null +++ b/vortex-array/src/arrays/widen/tests/decimal_cast.rs @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Decimal casts preserve lazy widening, checked precision, and nullability. +//! +//! These cases also cover decimal buffers whose stored width differs from their canonical width. + +use rstest::rstest; +use vortex_buffer::Buffer; +use vortex_buffer::buffer; +use vortex_error::VortexExpect; +use vortex_error::VortexResult; + +use super::SESSION; +use super::canonical; +use crate::ArrayRef; +use crate::IntoArray; +use crate::VortexSessionExecute; +use crate::arrays::BoolArray; +use crate::arrays::Decimal; +use crate::arrays::DecimalArray; +use crate::arrays::DictArray; +use crate::arrays::Widen; +use crate::arrays::WidenArray; +use crate::arrays::decimal::DecimalArrayExt; +use crate::arrays::widen::WidenArraySlotsExt; +use crate::assert_arrays_eq; +use crate::builtins::ArrayBuiltins; +use crate::dtype::DType; +use crate::dtype::DecimalDType; +use crate::dtype::Nullability; +use crate::dtype::PType; +use crate::dtype::i256; +use crate::expr::stats::Precision; +use crate::expr::stats::Stat; +use crate::expr::stats::StatsProvider; +use crate::scalar::DecimalValue; +use crate::scalar::Scalar; +use crate::scalar_fn::fns::cast::CastKernel; +use crate::scalar_fn::fns::cast::CastReduce; +use crate::validity::Validity; + +#[rstest] +#[case::i8(DecimalArray::from_iter([12i8, -12], DecimalDType::new(2, 0)), 4, vec![12, -12])] +#[case::i16(DecimalArray::from_iter([12i16, -12], DecimalDType::new(4, 0)), 9, vec![12, -12])] +#[case::i32(DecimalArray::from_iter([12i32, -12], DecimalDType::new(9, 0)), 18, vec![12, -12])] +#[case::i64(DecimalArray::from_iter([12i64, -12], DecimalDType::new(18, 2)), 38, vec![12, -12])] +#[case::i128(DecimalArray::from_iter([12i128, -12], DecimalDType::new(38, 0)), 76, vec![12, -12])] +#[case::stored_target_width( + DecimalArray::from_iter([12i16, -12], DecimalDType::new(2, 0)), + 4, + vec![12, -12], +)] +#[case::stored_narrower_than_source( + DecimalArray::from_iter([12i32, -12], DecimalDType::new(10, 2)), + 38, + vec![12, -12], +)] +#[case::stored_i256( + DecimalArray::from_iter( + [i256::from_i128(12), i256::from_i128(-12)], + DecimalDType::new(2, 0), + ), + 4, + vec![12, -12], +)] +#[case::empty(DecimalArray::from_iter(Vec::::new(), DecimalDType::new(2, 0)), 4, vec![])] +#[case::all_null( + DecimalArray::new(buffer![12i8, -12], DecimalDType::new(2, 0), Validity::AllInvalid), + 4, + vec![0, 0], +)] +fn test_precision_growth_is_lazy( + #[case] array: DecimalArray, + #[case] precision: u8, + #[case] expected: Vec, +) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let source_dtype = array.dtype().clone(); + let source_values_type = array.values_type(); + let source_ptr = array.buffer_handle().as_host().as_ptr(); + let validity = array.validity()?; + let decimal_dtype = DecimalDType::new(precision, array.decimal_dtype().scale()); + let target = DType::Decimal(decimal_dtype, validity.nullability()); + + let casted = array.into_array().cast(target)?; + + assert!(casted.is::()); + let widen = casted.as_::(); + let values = widen.values(); + assert_eq!(values.dtype(), &source_dtype); + assert_eq!(values.as_::().values_type(), source_values_type); + assert_eq!( + values.as_::().buffer_handle().as_host().as_ptr(), + source_ptr + ); + + let executed = casted.execute::(&mut ctx)?; + assert_eq!( + executed.values_type(), + decimal_dtype.canonical_values_type() + ); + let expected = DecimalArray::new(Buffer::from_iter(expected), decimal_dtype, validity); + assert_arrays_eq!(executed, expected, &mut ctx); + + Ok(()) +} + +#[rstest] +#[case::to_nullable(Validity::NonNullable, Nullability::Nullable)] +#[case::to_non_nullable(Validity::AllValid, Nullability::NonNullable)] +fn test_precision_growth_sets_child_nullability( + #[case] validity: Validity, + #[case] nullability: Nullability, +) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let array = DecimalArray::new(buffer![12i8, -12], DecimalDType::new(2, 0), validity); + let target_decimal = DecimalDType::new(4, 0); + let target = DType::Decimal(target_decimal, nullability); + + let casted = array.into_array().cast(target)?; + + assert!(casted.is::()); + assert_eq!( + casted.as_::().values().dtype().nullability(), + nullability + ); + let expected_validity = match nullability { + Nullability::Nullable => Validity::AllValid, + Nullability::NonNullable => Validity::NonNullable, + }; + let expected = DecimalArray::new(buffer![12i16, -12], target_decimal, expected_validity); + assert_arrays_eq!(casted, expected, &mut ctx); + + Ok(()) +} + +/// The child holds the same values as the source, so both cast paths keep its exact extrema, even +/// when the cast changes the nullability. +#[rstest] +#[case::reduce(false, Nullability::NonNullable)] +#[case::kernel_to_nullable(true, Nullability::Nullable)] +fn test_precision_growth_keeps_values_statistics( + #[case] through_kernel: bool, + #[case] nullability: Nullability, +) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let source_decimal = DecimalDType::new(4, 0); + let extremum = |value: i16, nullability| { + Scalar::decimal(DecimalValue::I16(value), source_decimal, nullability) + }; + let array = DecimalArray::from_iter([12i16, -12], source_decimal); + + for (stat, value) in [(Stat::Min, -12), (Stat::Max, 12)] { + let value = extremum(value, Nullability::NonNullable).into_value(); + let value = value.vortex_expect("the planted extremum is not null"); + array.statistics().set(stat, Precision::Exact(value)); + } + + let target = DType::Decimal(DecimalDType::new(18, 0), nullability); + let casted = if through_kernel { + ::cast(array.as_view(), &target, &mut ctx)? + } else { + ::cast(array.as_view(), &target)? + }; + let casted = casted.vortex_expect("same-scale precision growth casts"); + + assert!(casted.is::()); + let values = casted.as_::().values().clone(); + let statistics = values.statistics(); + let min = statistics.get(Stat::Min); + let max = statistics.get(Stat::Max); + assert_eq!(min, Precision::Exact(extremum(-12, nullability))); + assert_eq!(max, Precision::Exact(extremum(12, nullability))); + + Ok(()) +} + +#[rstest] +#[case::all_valid(true)] +#[case::null_row(false)] +fn test_encoded_validity_falls_back_to_kernel(#[case] second_valid: bool) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let validity = DictArray::try_new( + buffer![0u8, 1].into_array(), + BoolArray::from_iter([true, second_valid]).into_array(), + )?; + let array = DecimalArray::new( + buffer![12i8, -12], + DecimalDType::new(2, 0), + Validity::Array(validity.into_array()), + ); + let target_decimal = DecimalDType::new(4, 0); + let target = DType::Decimal(target_decimal, Nullability::NonNullable); + + assert!(::cast(array.as_view(), &target)?.is_none()); + let result = ::cast(array.as_view(), &target, &mut ctx); + if !second_valid { + let error = result.expect_err("the encoded validity contains a null row"); + assert!(error.to_string().contains("invalid values to non-nullable")); + + return Ok(()); + } + + let casted = result?.vortex_expect("same-scale precision growth has a cast kernel"); + assert!(casted.is::()); + assert_eq!( + casted.as_::().values().dtype().nullability(), + Nullability::NonNullable + ); + let expected = DecimalArray::from_iter([12i16, -12], target_decimal); + assert_arrays_eq!(casted, expected, &mut ctx); + + Ok(()) +} + +#[rstest] +#[case::narrower_than_parent(9)] +#[case::wider_than_parent(38)] +fn test_decimal_widen_retargets_child(#[case] precision: u8) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let values = DictArray::try_new( + buffer![0u8, 1, 0].into_array(), + DecimalArray::from_option_iter([Some(125i16), None], DecimalDType::new(4, 2)).into_array(), + )? + .into_array(); + let array = WidenArray::try_new( + values.clone(), + DType::Decimal(DecimalDType::new(18, 2), Nullability::Nullable), + )? + .into_array(); + let target_decimal = DecimalDType::new(precision, 2); + let target = DType::Decimal(target_decimal, Nullability::Nullable); + + let casted = array.cast(target)?; + + assert!(casted.is::()); + assert!(ArrayRef::ptr_eq(casted.as_::().values(), &values)); + let expected = DecimalArray::from_option_iter([Some(125i128), None, Some(125)], target_decimal); + assert_arrays_eq!(casted, expected, &mut ctx); + + Ok(()) +} + +#[rstest] +#[case::smaller_decimal(DType::Decimal(DecimalDType::new(3, 2), Nullability::Nullable))] +#[case::rescale(DType::Decimal(DecimalDType::new(18, 3), Nullability::Nullable))] +#[case::integer(DType::Primitive(PType::I64, Nullability::Nullable))] +#[case::float(DType::Primitive(PType::F64, Nullability::Nullable))] +fn test_decimal_widen_cast_matches_canonical(#[case] target: DType) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let values = + DecimalArray::from_option_iter([Some(125i16), None, Some(-250)], DecimalDType::new(4, 2)); + let array = WidenArray::try_new( + values.into_array(), + DType::Decimal(DecimalDType::new(18, 2), Nullability::Nullable), + )? + .into_array(); + let expected = canonical(&array, &mut ctx)?.cast(target.clone())?; + + let casted = array.cast(target)?; + + assert_arrays_eq!(casted, expected, &mut ctx); + + Ok(()) +} + +#[test] +fn test_precision_narrowing_checks_values() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let values = DecimalArray::from_iter([1000i16], DecimalDType::new(4, 0)); + let array = WidenArray::try_new( + values.into_array(), + DType::Decimal(DecimalDType::new(18, 0), Nullability::NonNullable), + )? + .into_array(); + let target = DType::Decimal(DecimalDType::new(3, 0), Nullability::NonNullable); + + let error = array + .cast(target)? + .execute::(&mut ctx) + .expect_err("1000 does not fit a precision of three digits"); + + assert!(error.to_string().contains("does not fit in precision")); + + Ok(()) +} + +#[test] +fn test_repeated_casts_terminate_without_nested_widen() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let source = DecimalArray::from_iter([12i8, -12], DecimalDType::new(2, 0)).into_array(); + let mut array = source.clone(); + + for precision in [4, 9, 18, 38, 76, 4, 2] { + let decimal_dtype = DecimalDType::new(precision, 0); + let target = DType::Decimal(decimal_dtype, Nullability::NonNullable); + array = array.cast(target)?; + if let Some(widen) = array.as_opt::() { + assert!(!widen.values().is::()); + assert_eq!(widen.values().dtype(), source.dtype()); + } + + assert_arrays_eq!( + array, + DecimalArray::from_iter([12i128, -12], decimal_dtype), + &mut ctx + ); + } + + Ok(()) +} + +#[rstest] +#[case::reuses_canonical_storage( + DecimalArray::from_iter([12i64, -12], DecimalDType::new(10, 2)), + DecimalArray::from_iter([12i64, -12], DecimalDType::new(18, 2)), + true, +)] +#[case::reuses_wider_storage( + DecimalArray::from_iter([12i128, -12], DecimalDType::new(10, 0)), + DecimalArray::from_iter([12i128, -12], DecimalDType::new(18, 0)), + true, +)] +#[case::copies_narrow_storage( + DecimalArray::from_iter([125i32, -250], DecimalDType::new(10, 2)), + DecimalArray::from_iter([125i64, -250], DecimalDType::new(18, 2)), + false, +)] +fn test_same_canonical_width( + #[case] array: DecimalArray, + #[case] expected: DecimalArray, + #[case] reuses_buffer: bool, +) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let source_ptr = array.buffer_handle().as_host().as_ptr(); + let casted = array.into_array().cast(expected.dtype().clone())?; + assert!(!casted.is::()); + + let executed = casted.execute::(&mut ctx)?; + + assert_eq!(executed.values_type(), expected.values_type()); + assert_eq!( + executed.buffer_handle().as_host().as_ptr() == source_ptr, + reuses_buffer + ); + assert_arrays_eq!(executed, expected, &mut ctx); + + Ok(()) +} diff --git a/vortex-array/src/arrays/widen/tests/decimal_compare.rs b/vortex-array/src/arrays/widen/tests/decimal_compare.rs index cd71ebc1851..1b0625bb530 100644 --- a/vortex-array/src/arrays/widen/tests/decimal_compare.rs +++ b/vortex-array/src/arrays/widen/tests/decimal_compare.rs @@ -137,6 +137,8 @@ fn test_compare_constant( assert_compare_matches_canonical(&array, &constant, operator, &mut ctx) } +/// In `different_widths`, the cast of the `i8` child to the common precision returns a `Widen` of +/// its own, so the compare runs through the fallback. #[rstest] #[case::same_width( DecimalArray::from_iter( diff --git a/vortex-array/src/arrays/widen/tests/mod.rs b/vortex-array/src/arrays/widen/tests/mod.rs index 57c8b91508d..94299d64f52 100644 --- a/vortex-array/src/arrays/widen/tests/mod.rs +++ b/vortex-array/src/arrays/widen/tests/mod.rs @@ -11,6 +11,7 @@ mod aggregates; mod compute; mod decimal; mod decimal_aggregates; +mod decimal_cast; mod decimal_compare; mod decimal_fallback; mod decimal_stored; diff --git a/vortex-file/src/tests.rs b/vortex-file/src/tests.rs index c015c706b7d..1dcf89a916e 100644 --- a/vortex-file/src/tests.rs +++ b/vortex-file/src/tests.rs @@ -35,6 +35,7 @@ use vortex_array::arrays::StructArray; use vortex_array::arrays::TemporalArray; use vortex_array::arrays::VarBinArray; use vortex_array::arrays::VarBinViewArray; +use vortex_array::arrays::WidenArray; use vortex_array::arrays::dict::DictArraySlotsExt; use vortex_array::arrays::struct_::StructArrayExt; use vortex_array::assert_arrays_eq; @@ -259,6 +260,35 @@ async fn test_read_simple() { assert_arrays_eq!(result, st, &mut SESSION.create_execution_ctx()); } +#[rstest] +#[case::non_nullable(Validity::NonNullable)] +#[case::partially_valid(Validity::from_iter([true, false, true, true, false]))] +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn test_decimal_widen_default_writer_roundtrip( + #[case] validity: Validity, +) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let decimal_dtype = DecimalDType::new(18, 2); + let values = DecimalArray::new( + buffer![-125i16, 50, 0, 250, 900], + DecimalDType::new(4, 2), + validity.clone(), + ); + let dtype = DType::Decimal(decimal_dtype, validity.nullability()); + let widened = WidenArray::try_new(values.into_array(), dtype)?.into_array(); + let array = StructArray::from_fields(&[("decimal", widened)])?.into_array(); + let expected_values = + DecimalArray::new(buffer![-125i64, 50, 0, 250, 900], decimal_dtype, validity); + let expected = StructArray::from_fields(&[("decimal", expected_values.into_array())])?; + + let actual = write_read_roundtrip(array).await?; + + assert_arrays_eq!(actual, expected, &mut ctx); + + Ok(()) +} + /// Wide decimals split into multi-part arrays only when the writer allows the v2 format. The /// default writer allows every registered encoding once editions are disabled; a strategy built /// from the session keeps the enabled editions' restrictions regardless. From 7d49b0da06167b1208250e5f6cfb0a1232f4e86f Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 12:45:12 -0500 Subject: [PATCH 30/41] docs(arrow): explain why dictionary imports need special handling (#11233) ## Summary - Tracking Issue: #11114 Follow-up to [the dictionary and run-end discussion in #11185](https://github.com/vortex-data/vortex/pull/11185#discussion_r4469686840). ## Changes Explains why dictionary values need the outer field's extension metadata before importer dispatch, while run-end values have their own field and use normal nested-field imports. Documentation only. Tests, linting, and formatting were not run. Local commit and push hooks were skipped. Signed-off-by: Connor Tsui --- vortex-arrow/src/session.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vortex-arrow/src/session.rs b/vortex-arrow/src/session.rs index ec74a598ea7..9606a367913 100644 --- a/vortex-arrow/src/session.rs +++ b/vortex-arrow/src/session.rs @@ -191,6 +191,11 @@ pub trait ArrowExportVTable: 'static + Send + Sync + Debug { /// field, with the dictionary field's metadata, and the values array. The session then wraps the /// plugin's result in a [`DictArray`] with the dictionary's codes. /// +/// Arrow stores dictionary values as a bare [`DataType`], so their extension metadata lives on +/// the outer [`Field`]. Peeling the dictionary lets the importer see the value type described by +/// that metadata. [`DataType::RunEndEncoded`] has a separate values field that can carry its own +/// extension metadata, so its values use the normal nested-field import path. +/// /// This is purely an implementation trait, its methods should not be called directly. Instead, /// use the methods on [`ArrowSession`]. pub trait ArrowImportVTable: 'static + Send + Sync + Debug { From 9e6770dbe4e09394a5ea51b624d74e84aaaf4d08 Mon Sep 17 00:00:00 2001 From: Joe Isaacs Date: Wed, 4 Nov 2026 17:51:53 +0000 Subject: [PATCH 31/41] Push DataFusion dynamic filters into V2 scans (#11215) Signed-off-by: Joe Isaacs --- vortex-datafusion/src/persistent/opener.rs | 81 ++++++++- vortex-datafusion/src/persistent/scan_v2.rs | 13 +- vortex-datafusion/src/persistent/source.rs | 91 +++++++++- vortex-datafusion/src/v2/dynamic_filters.rs | 119 ++++++++++++- vortex-datafusion/src/v2/mod.rs | 2 +- vortex-layout/src/scan/execution/filter.rs | 87 +++++++++- vortex-layout/src/scan/execution/prune.rs | 2 +- vortex-layout/src/scan/filter.rs | 119 +++++++++++++ vortex-layout/src/scan/v2/mod.rs | 32 +++- vortex-layout/src/scan/v2/tests.rs | 176 ++++++++++++++++++-- 10 files changed, 672 insertions(+), 50 deletions(-) diff --git a/vortex-datafusion/src/persistent/opener.rs b/vortex-datafusion/src/persistent/opener.rs index 208691f0b0e..46982a9cf7b 100644 --- a/vortex-datafusion/src/persistent/opener.rs +++ b/vortex-datafusion/src/persistent/opener.rs @@ -23,6 +23,7 @@ use datafusion_datasource::file_stream::FileOpenFuture; use datafusion_datasource::file_stream::FileOpener; use datafusion_execution::cache::cache_manager::CachedFileMetadataEntry; use datafusion_execution::cache::cache_manager::FileMetadataCache; +use datafusion_physical_expr::DynamicFilterTracking; use datafusion_physical_expr::PhysicalExprRef; use datafusion_physical_expr::expressions::LambdaExpr; use datafusion_physical_expr::projection::ProjectionExprs; @@ -50,6 +51,7 @@ use tracing::Instrument; use vortex::array::VortexSessionExecute; use vortex::error::VortexError; use vortex::error::VortexExpect; +use vortex::expr::Expression; use vortex::expr::and_collect; use vortex::file::OpenOptionsSessionExt; use vortex::io::InstrumentedReadAt; @@ -84,6 +86,7 @@ use crate::persistent::cache::CachedVortexMetadata; use crate::persistent::reader::VortexReaderFactory; use crate::persistent::scan_v2::ScanDriver; use crate::persistent::stream::PrunableStream; +use crate::v2::dynamic_filters::convert_dynamic_filter; #[derive(Clone)] pub(crate) struct VortexOpener { @@ -128,6 +131,10 @@ pub(crate) struct VortexOpener { pub natural_splits: Arc>>, /// Shared physical Arrow schemas of each file, keyed by file path. pub file_schemas: Arc>, + /// The Vortex expression of each dynamic filter of `filter`, converted once for every file + /// and partition of the scan. Sharing it lets the scan cache of each file keep the filter's + /// plans for every partition that scans the file. + pub dynamic_filters: Arc>, /// Whether the query has output ordering specified pub has_output_ordering: bool, @@ -142,6 +149,51 @@ pub(crate) struct VortexOpener { pub partitions: usize, } +/// Identifies a dynamic filter of a scan's predicate by its allocation, which the predicate keeps +/// alive for as long as the scan. +pub(crate) type DynamicFilterKey = usize; + +impl VortexOpener { + /// The predicate without its dynamic filters, and each dynamic filter's columns and Vortex + /// expression. A dynamic filter is converted once, against the table schema, for the whole + /// scan. + fn split_dynamic_filters(&self) -> (Option, Vec<(Vec, Expression)>) { + let Some(filter) = &self.filter else { + return (None, Vec::new()); + }; + let (dynamic, exact): (Vec<_>, Vec<_>) = + split_conjunction(filter).into_iter().partition(|conjunct| { + DynamicFilterTracking::classify(conjunct).contains_dynamic_filter() + }); + if dynamic.is_empty() { + return (Some(Arc::clone(filter)), Vec::new()); + } + let dynamic = dynamic + .into_iter() + .map(|conjunct| { + let columns = collect_columns(conjunct) + .into_iter() + .map(|column| column.name().to_string()) + .collect(); + let key = Arc::as_ptr(conjunct).cast::<()>().addr(); + let expression = self + .dynamic_filters + .entry(key) + .or_insert_with(|| { + convert_dynamic_filter( + Arc::clone(conjunct), + Arc::clone(self.table_schema.file_schema()), + Arc::clone(&self.expression_converter), + ) + }) + .clone(); + (columns, expression) + }) + .collect(); + (conjunction_opt(exact.into_iter().cloned()), dynamic) + } +} + impl FileOpener for VortexOpener { fn open(&self, file: PartitionedFile) -> DFResult { // Calculate the output schema before replacing partition columns with literals so it @@ -158,7 +210,7 @@ impl FileOpener for VortexOpener { ]; let mut projection = self.projection.clone(); - let mut filter = self.filter.clone(); + let (mut filter, dynamic_filters) = self.split_dynamic_filters(); let reader = self.vortex_reader_factory.create_reader(&file, &session)?; @@ -344,6 +396,19 @@ impl FileOpener for VortexOpener { } } } + // The operator that produces a dynamic filter enforces it exactly, so the scan + // evaluates it only where the file has every column it reads. + native_filters.extend( + dynamic_filters + .iter() + .filter(|(columns, _)| { + columns.iter().all(|column| { + this_file_schema.field_with_name(column).is_ok() + && !literal_value_cols.contains_key(column) + }) + }) + .map(|(_, expression)| expression.clone()), + ); let residual_filter = conjunction_opt(residual_filters); let native_filter = and_collect(native_filters) .map(|filter| filter.bind(vxf.dtype())?.optimize_recursive()) @@ -443,7 +508,11 @@ impl FileOpener for VortexOpener { scan_file.cache = Some(shared_scan_cache(&scan_caches, &file.object_meta.location)); let mut builder = V2ScanBuilder::new(vxf.session().clone(), scan_file) .with_projection(scan_projection) - .with_some_filter(native_filter); + .with_some_filter(native_filter) + // Dynamic filters are pushed down inexactly: the TopK or join that produces + // one applies it again, so the scan may stop evaluating one that rejects + // almost nothing. + .with_skip_unselective_dynamic(true); if let Some(bytes) = byte_range { // Every partition maps the same byte boundary to the same row boundary. let rows = partition_row_range(bytes, file.object_meta.size, vxf.row_count()); @@ -1150,6 +1219,7 @@ mod tests { partitions: 1, natural_splits: Default::default(), file_schemas: Default::default(), + dynamic_filters: Default::default(), has_output_ordering: false, expression_converter: Arc::new(DefaultExpressionConverter::default()), file_metadata_cache: None, @@ -1560,6 +1630,7 @@ mod tests { partitions: 1, natural_splits: Default::default(), file_schemas: Default::default(), + dynamic_filters: Default::default(), has_output_ordering: false, expression_converter: Arc::new(DefaultExpressionConverter::default()), file_metadata_cache: None, @@ -1652,6 +1723,7 @@ mod tests { partitions: 1, natural_splits: Default::default(), file_schemas: Default::default(), + dynamic_filters: Default::default(), has_output_ordering: false, expression_converter: Arc::new(DefaultExpressionConverter::default()), file_metadata_cache: None, @@ -1835,6 +1907,7 @@ mod tests { partitions: 1, natural_splits: Default::default(), file_schemas: Default::default(), + dynamic_filters: Default::default(), has_output_ordering: false, expression_converter: Arc::new(DefaultExpressionConverter::default()), file_metadata_cache: None, @@ -1900,6 +1973,7 @@ mod tests { partitions: 1, natural_splits: Default::default(), file_schemas: Default::default(), + dynamic_filters: Default::default(), has_output_ordering: false, expression_converter: Arc::new(DefaultExpressionConverter::default()), file_metadata_cache: None, @@ -2116,6 +2190,7 @@ mod tests { partitions: 1, natural_splits: Default::default(), file_schemas: Default::default(), + dynamic_filters: Default::default(), has_output_ordering: false, expression_converter: Arc::new(DefaultExpressionConverter::default()), file_metadata_cache: None, @@ -2290,7 +2365,7 @@ mod tests { &self, _expr: &PhysicalExprRef, _schema: &Schema, - ) -> DFResult> { + ) -> DFResult> { Ok(None) } } diff --git a/vortex-datafusion/src/persistent/scan_v2.rs b/vortex-datafusion/src/persistent/scan_v2.rs index 10ea7b969ea..86bf59179ad 100644 --- a/vortex-datafusion/src/persistent/scan_v2.rs +++ b/vortex-datafusion/src/persistent/scan_v2.rs @@ -25,6 +25,7 @@ use futures::stream::BoxStream; use vortex::array::ArrayRef; use vortex::error::VortexResult; use vortex::error::vortex_err; +use vortex::io::request::IoService; use vortex::io::request::IoSource; use vortex::io::runtime::Handle; use vortex::layout::scan::v2::PreparedScan; @@ -73,7 +74,7 @@ impl ScanDriver { let io = scan.io(); let (chunk_handle, chunk_io, chunk_map) = (self.handle.clone(), Arc::clone(&io), Arc::clone(&map)); - let batches = run_stream(self.handle, root, scope, io.session(), map, inline) + let batches = run_stream(self.handle, root, scope, Arc::clone(&io), map, inline) .map_ok(move |yielded| match yielded { Yielded::Batch(start, batch) => { stream::once(future::ready(Ok((start, batch)))).boxed() @@ -128,7 +129,9 @@ struct RunState { handle: Handle, /// Taken while a CPU step runs on its task and put back with its result. run: Option, - io: Arc, + /// The scan's reads: each root of the run reads through a session of its own, which the run + /// clears when the root retires. + io: Arc, map: MapBatch, /// Whether a spawned chunk runs on this run, as another root, instead of being yielded. inline: bool, @@ -143,12 +146,12 @@ fn run_stream( handle: Handle, root: Box, scope: WorkScope, - io: Arc, + io: Arc, map: MapBatch, inline: bool, ) -> BoxStream<'static, VortexResult> { let mut run = Run::new(); - run.admit(root, scope, Arc::clone(&io)); + run.admit(root, scope, io.session()); let state = RunState { handle, run: Some(run), @@ -187,7 +190,7 @@ fn run_stream( return Ok(Some((Yielded::Batch(start, batch), state))); } Step::Spawn(scope, child) if state.inline => { - run.admit(child, scope, Arc::clone(&state.io)); + run.admit(child, scope, state.io.session()); } Step::Spawn(scope, child) => { return Ok(Some((Yielded::Spawn(scope, child), state))); diff --git a/vortex-datafusion/src/persistent/source.rs b/vortex-datafusion/src/persistent/source.rs index 644fea4e71c..649bae3326b 100644 --- a/vortex-datafusion/src/persistent/source.rs +++ b/vortex-datafusion/src/persistent/source.rs @@ -15,6 +15,7 @@ use datafusion_datasource::file_groups::FileGroupPartitioner; use datafusion_datasource::file_scan_config::FileScanConfig; use datafusion_datasource::file_stream::FileOpener; use datafusion_execution::cache::cache_manager::FileMetadataCache; +use datafusion_physical_expr::DynamicFilterTracking; use datafusion_physical_expr::EquivalenceProperties; use datafusion_physical_expr::LexOrdering; use datafusion_physical_expr::PhysicalExprRef; @@ -32,6 +33,7 @@ use datafusion_physical_plan::filter_pushdown::PushedDownPredicate; use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet; use object_store::ObjectStore; use object_store::path::Path; +use vortex::expr::Expression; use vortex::file::VORTEX_FILE_EXTENSION; use vortex::layout::LayoutReader; use vortex::layout::scan::v2::WeakScanCache; @@ -46,6 +48,7 @@ use super::opener::VortexOpener; use crate::VortexTableOptions; use crate::convert::exprs::DefaultExpressionConverter; use crate::convert::exprs::ExpressionConverter; +use crate::persistent::opener::DynamicFilterKey; use crate::persistent::reader::DefaultVortexReaderFactory; use crate::persistent::reader::VortexReaderFactory; @@ -209,6 +212,8 @@ pub struct VortexSource { natural_splits: Arc>>, /// Shared physical Arrow schemas of each file, keyed by path. file_schemas: Arc>, + /// The Vortex expression of each dynamic filter of the predicate, keyed by the filter. + dynamic_filters: Arc>, expression_converter: Arc, pub(crate) vortex_reader_factory: Option>, pub(crate) ordered: bool, @@ -245,6 +250,7 @@ impl VortexSource { scan_caches: Arc::new(DashMap::default()), natural_splits: Arc::new(DashMap::default()), file_schemas: Arc::new(DashMap::default()), + dynamic_filters: Arc::new(DashMap::default()), expression_converter, vortex_reader_factory: None, vx_metrics_registry: Arc::new(DefaultMetricsRegistry::default()), @@ -372,6 +378,7 @@ impl VortexSource { scan_caches: Arc::clone(&self.scan_caches), natural_splits: Arc::clone(&self.natural_splits), file_schemas: Arc::clone(&self.file_schemas), + dynamic_filters: Arc::clone(&self.dynamic_filters), has_output_ordering: !base_config.output_ordering.is_empty() || self.ordered, expression_converter: Arc::clone(&self.expression_converter), file_metadata_cache: self.file_metadata_cache.clone(), @@ -511,10 +518,19 @@ impl FileSource for VortexSource { .with_updated_node(Arc::new(source) as _)); } + // A dynamic filter, such as the bound a top-K tightens while it runs, stays with the + // operator that produces it, which enforces it exactly. A V2 scan also evaluates it, from + // its current value, to prune and filter what that operator would discard. + let mut dynamic = Vec::new(); let supported_filters = filters .into_iter() .map(|expr| { - if self + if self.options.scan_v2 + && DynamicFilterTracking::classify(&expr).contains_dynamic_filter() + { + dynamic.push(Arc::clone(&expr)); + Ok(PushedDownPredicate::unsupported(expr)) + } else if self .expression_converter .try_convert(&expr, self.table_schema.table_schema())? .is_some() @@ -526,9 +542,10 @@ impl FileSource for VortexSource { }) .collect::>>()?; - if supported_filters - .iter() - .all(|p| matches!(p.discriminant, PushedDown::No)) + if dynamic.is_empty() + && supported_filters + .iter() + .all(|p| matches!(p.discriminant, PushedDown::No)) { return Ok(FilterPushdownPropagation::with_parent_pushdown_result( vec![PushedDown::No; supported_filters.len()], @@ -542,7 +559,8 @@ impl FileSource for VortexSource { PushedDown::Yes => Some(&p.predicate), PushedDown::No => None, }) - .cloned(); + .cloned() + .chain(dynamic); let predicate = match source.vortex_predicate { Some(predicate) => conjunction(std::iter::once(predicate).chain(supported)), @@ -605,6 +623,7 @@ mod tests { use datafusion_physical_expr::ScalarFunctionExpr; use datafusion_physical_expr::expressions as df_expr; use datafusion_physical_expr::expressions::Column; + use datafusion_physical_expr::expressions::DynamicFilterPhysicalExpr; use object_store::memory::InMemory; use vortex::VortexSessionDefault; @@ -620,7 +639,7 @@ mod tests { &self, expr: &PhysicalExprRef, schema: &Schema, - ) -> DFResult> { + ) -> DFResult> { self.inner.try_convert(expr, schema) } @@ -754,6 +773,66 @@ mod tests { Ok(()) } + /// A V1 scan leaves a dynamic filter to the operator that produces it. + #[test] + fn try_pushdown_filters_leaves_dynamic_filters_out_of_v1_scans() -> anyhow::Result<()> { + let schema = Arc::new(Schema::new(vec![Field::new("name", DataType::Utf8, false)])); + let source = sort_test_source(Arc::clone(&schema)); + let dynamic = Arc::new(DynamicFilterPhysicalExpr::new( + vec![Arc::new(Column::new("name", 0)) as Arc], + Arc::new(df_expr::Literal::new(ScalarValue::Boolean(Some(true)))), + )) as Arc; + + let result = source.try_pushdown_filters(vec![dynamic], &ConfigOptions::new())?; + + assert!(matches!(result.filters.as_slice(), [PushedDown::No])); + let updated_source = result + .updated_node + .ok_or_else(|| anyhow::anyhow!("expected updated VortexSource"))? + .downcast_ref::() + .ok_or_else(|| anyhow::anyhow!("expected VortexSource"))? + .clone(); + assert!(updated_source.vortex_predicate.is_none()); + Ok(()) + } + + /// A dynamic filter stays with the operator that produces it, but a V2 scan also evaluates + /// its current value, alongside the filters pushed down exactly. + #[test] + fn try_pushdown_filters_scans_with_dynamic_filters() -> anyhow::Result<()> { + let schema = Arc::new(Schema::new(vec![Field::new("name", DataType::Utf8, false)])); + let source = sort_test_source(Arc::clone(&schema)).with_options(VortexTableOptions { + scan_v2: true, + ..Default::default() + }); + let exact = octet_length_filter(&schema); + let dynamic = Arc::new(DynamicFilterPhysicalExpr::new( + vec![Arc::new(Column::new("name", 0)) as Arc], + Arc::new(df_expr::Literal::new(ScalarValue::Boolean(Some(true)))), + )) as Arc; + + let result = source.try_pushdown_filters( + vec![Arc::clone(&exact), Arc::clone(&dynamic)], + &ConfigOptions::new(), + )?; + + assert!(matches!( + result.filters.as_slice(), + [PushedDown::Yes, PushedDown::No] + )); + let updated_source = result + .updated_node + .ok_or_else(|| anyhow::anyhow!("expected updated VortexSource"))? + .downcast_ref::() + .ok_or_else(|| anyhow::anyhow!("expected VortexSource"))? + .clone(); + assert_eq!( + updated_source.vortex_predicate.as_ref(), + Some(&conjunction([exact, dynamic])) + ); + Ok(()) + } + #[test] fn try_pushdown_filters_respects_disabled_predicate_pushdown() -> anyhow::Result<()> { let schema = Arc::new(Schema::new(vec![Field::new("name", DataType::Utf8, false)])); diff --git a/vortex-datafusion/src/v2/dynamic_filters.rs b/vortex-datafusion/src/v2/dynamic_filters.rs index 5ab263a1082..b8af6ac8a78 100644 --- a/vortex-datafusion/src/v2/dynamic_filters.rs +++ b/vortex-datafusion/src/v2/dynamic_filters.rs @@ -9,6 +9,7 @@ use datafusion_expr::Operator as DFOperator; use datafusion_physical_expr::DynamicFilterTracking; use datafusion_physical_expr::PhysicalExpr; use datafusion_physical_expr::expressions::BinaryExpr; +use datafusion_physical_expr::split_conjunction; use datafusion_physical_expr::utils::collect_columns; use datafusion_physical_expr_common::physical_expr::snapshot_generation; use datafusion_physical_expr_common::physical_expr::snapshot_physical_expr; @@ -23,6 +24,7 @@ use vortex::expr::get_item; use vortex::expr::lit; use vortex::expr::pack; use vortex::expr::root; +use vortex::expr::transform::find_between; use vortex::expr::transform::replace; use vortex::scalar_fn::fns::dynamic_expression::DynamicExpressionSource; use vortex::scalar_fn::fns::dynamic_expression::input_field_name; @@ -31,7 +33,7 @@ use vortex::scalar_fn::fns::operators::Operator; use crate::convert::ExpressionConverter; use crate::convert::exprs::is_infallible; -pub(super) fn convert_dynamic_filter( +pub(crate) fn convert_dynamic_filter( expression: Arc, schema: SchemaRef, converter: Arc, @@ -117,15 +119,122 @@ impl DynamicExpressionSource for DataFusionDynamicFilter { fn current(&self, scope: &DType) -> VortexResult { let converted = snapshot_physical_expr(Arc::clone(&self.expression)) - .and_then(|snapshot| self.converter.try_convert(&snapshot, &self.schema)) .ok() - .flatten() - .filter(is_infallible); + .and_then(|snapshot| self.convert_conjuncts(&snapshot)); // The producing DataFusion operator retains exact enforcement when conversion fails. let expression = converted.map_or_else( || lit(true), |expr| replace(expr, &root(), self.scope_projection.clone()), ); - expression.bind(scope) + // A join bounds its probe keys from both sides. Each comparison would decode the keys + // again, while a between decodes them once. + find_between(expression.bind(scope)?) + } +} + +impl DataFusionDynamicFilter { + /// Converts each conjunct of `snapshot` that Vortex can evaluate. + /// + /// A join's dynamic filter bounds the probe side's keys and tests them against the build side + /// with a hash table lookup, which has no Vortex equivalent. Dropping a conjunct only widens + /// the filter, and the join enforces the keys exactly. + fn convert_conjuncts(&self, snapshot: &Arc) -> Option { + split_conjunction(snapshot) + .into_iter() + .filter_map(|conjunct| { + self.converter + .try_convert(conjunct, &self.schema) + .ok() + .flatten() + .filter(is_infallible) + }) + .reduce(|left, right| binary(Operator::And, left, right)) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use arrow_schema::DataType; + use arrow_schema::Field; + use arrow_schema::Schema; + use datafusion_common::ScalarValue; + use datafusion_expr::Operator as DFOperator; + use datafusion_physical_expr::PhysicalExpr; + use datafusion_physical_expr::expressions::BinaryExpr; + use datafusion_physical_expr::expressions::Column; + use datafusion_physical_expr::expressions::Literal; + use datafusion_physical_expr::expressions::NoOp; + use vortex::dtype::DType; + use vortex::dtype::Nullability; + use vortex::dtype::PType; + use vortex::error::VortexResult; + use vortex::expr::Expression; + use vortex::expr::root; + use vortex::scalar_fn::fns::between::Between; + use vortex::scalar_fn::fns::dynamic_expression::DynamicExpressionSource; + + use super::DataFusionDynamicFilter; + use crate::convert::exprs::DefaultExpressionConverter; + + fn and(left: Arc, right: Arc) -> Arc { + Arc::new(BinaryExpr::new(left, DFOperator::And, right)) + } + + fn at_least(value: i32) -> Arc { + Arc::new(BinaryExpr::new( + Arc::new(Column::new("key", 0)), + DFOperator::GtEq, + Arc::new(Literal::new(ScalarValue::Int32(Some(value)))), + )) + } + + fn at_most(value: i32) -> Arc { + Arc::new(BinaryExpr::new( + Arc::new(Column::new("key", 0)), + DFOperator::LtEq, + Arc::new(Literal::new(ScalarValue::Int32(Some(value)))), + )) + } + + fn filter(expression: Arc) -> DataFusionDynamicFilter { + let schema = Arc::new(Schema::new(vec![Field::new("key", DataType::Int32, false)])); + DataFusionDynamicFilter { + expression, + schema, + scope_projection: root(), + converter: Arc::new(DefaultExpressionConverter::default()), + } + } + + fn convert(expression: Arc) -> Option { + filter(Arc::clone(&expression)).convert_conjuncts(&expression) + } + + #[test] + fn keeps_bounds_beside_an_unsupported_conjunct() { + let converted = convert(and(at_least(1), Arc::new(NoOp::new()))); + assert_eq!(converted, convert(at_least(1))); + assert_ne!(converted, None); + } + + #[test] + fn converts_nothing_without_a_supported_conjunct() { + assert_eq!(convert(Arc::new(NoOp::new())), None); + } + + #[test] + fn merges_key_bounds_into_a_between() -> VortexResult<()> { + let scope = DType::struct_( + [( + "key", + DType::Primitive(PType::I32, Nullability::NonNullable), + )], + Nullability::NonNullable, + ); + let current = filter(and(at_least(1), at_most(5))).current(&scope)?; + assert!(current.is::(), "{current}"); + Ok(()) } } diff --git a/vortex-datafusion/src/v2/mod.rs b/vortex-datafusion/src/v2/mod.rs index 176bb1295f1..7f16da3ed84 100644 --- a/vortex-datafusion/src/v2/mod.rs +++ b/vortex-datafusion/src/v2/mod.rs @@ -33,7 +33,7 @@ //! [`TableProvider`]: datafusion_catalog::TableProvider //! [`DataSource`]: datafusion_datasource::source::DataSource -mod dynamic_filters; +pub(crate) mod dynamic_filters; mod source; mod table; diff --git a/vortex-layout/src/scan/execution/filter.rs b/vortex-layout/src/scan/execution/filter.rs index 4278d31001d..f4e50ba5ff4 100644 --- a/vortex-layout/src/scan/execution/filter.rs +++ b/vortex-layout/src/scan/execution/filter.rs @@ -26,6 +26,7 @@ use vortex_scan::planning::planner::PlannerOutput; use vortex_scan::planning::planner::State; use vortex_session::VortexSession; +use super::FILTERED_ROWS_PER_CHUNK; use super::PreparedScan; use super::SegmentLocation; use super::SelectedRows; @@ -53,12 +54,21 @@ pub struct FilterPlans { } impl FilterPlans { + /// Plans `expression`'s conjuncts over `root`. + /// + /// With `skip_unselective_dynamic`, a dynamic conjunct that kept almost every row on its last + /// chunks is switched off, skipping both its zone pruning and its evaluation, until its dynamic + /// values change. Only enable it when the producer of the dynamic expressions applies them + /// again. [`find_between`] only merges comparisons against literals, so a switched-off + /// conjunct never carries a static restriction with it. pub(crate) fn new( expression: BoundExpression, root: &PlanRef, session: &VortexSession, + skip_unselective_dynamic: bool, ) -> VortexResult { - let expression = FilterExpr::new(find_between(expression)?); + let expression = FilterExpr::new(find_between(expression)?) + .with_skip_unselective_dynamic(skip_unselective_dynamic); let predicates = expression .conjuncts() .iter() @@ -95,10 +105,39 @@ impl FilterPlans { }) } - /// The pruning plans, run once over a scan's rows before its chunks are planned. + /// Whether dynamic conjuncts that keep almost every row are switched off. + pub(crate) fn skips_unselective_dynamic(&self) -> bool { + self.expression.skips_unselective_dynamic() + } + + /// Whether the conjunct is a dynamic conjunct that is currently switched off. + #[cfg(test)] + pub(crate) fn is_switched_off(&self, conjunct_idx: usize) -> bool { + self.expression.is_switched_off(conjunct_idx) + } + + /// The pruning plans of every conjunct that has one. + #[cfg(test)] pub(crate) fn pruning(&self) -> impl Iterator { self.pruning.iter().flatten() } + + /// The pruning plans worth running before a scan of `rows` rows. + /// + /// A scan of at most one filtered chunk evaluates its conjuncts on that chunk next, so + /// pruning it first saves little and delays the chunk. Pruning a dynamic conjunct such as a + /// join's key bounds also proves its current predicate over the zones again. Without those + /// plans, TPC-DS at SF1, whose join probes are mostly scans of one chunk, ran 3% faster, and + /// SF10 was unchanged. + pub(crate) fn pruning_over(&self, rows: u64) -> impl Iterator { + let small = rows <= FILTERED_ROWS_PER_CHUNK as u64; + self.pruning + .iter() + .enumerate() + .filter(move |(idx, _)| !small || self.expression.dynamic_updates(*idx).is_none()) + .filter(|(idx, _)| !self.expression.is_switched_off(*idx)) + .filter_map(|(_, plan)| plan.as_ref()) + } } /// A pruning plan must not fetch data when its requested statistics are unavailable. @@ -130,7 +169,8 @@ pub(super) struct FilterPlanner { selected: Option, filter: Arc, remaining: BitVec, - active: Option<(usize, ScanAdaptor)>, + /// The conjunct being evaluated, its dynamic version when it has one, and its scan. + active: Option<(usize, Option, ScanAdaptor)>, pieces: Vec, announced: bool, /// Ids for hints, counting down from `u32::MAX` so they never meet an adaptor's fetch ids. @@ -200,7 +240,7 @@ pub(super) fn piece_mask(session: &VortexSession, piece: Piece) -> VortexResult< impl IoConsumer for FilterPlanner { fn set_io_result(&mut self, request: IoRequestId, result: IoResult) { - if let Some((_, adaptor)) = &mut self.active { + if let Some((_, _, adaptor)) = &mut self.active { adaptor.set_io_result(request, result); } } @@ -212,7 +252,7 @@ impl Planner for FilterPlanner { return State::Done; } match &self.active { - Some((_, adaptor)) if adaptor.state() != State::Done => adaptor.state(), + Some((_, _, adaptor)) if adaptor.state() != State::Done => adaptor.state(), _ => State::NeedsCompute, } } @@ -225,9 +265,15 @@ impl Planner for FilterPlanner { self.announced = true; let session = &self.scan.session; let rows = selected.scope.rows.clone(); + let expression = &self.filter.expression; let predicates = segments_read( session, - self.filter.predicates.iter(), + self.filter + .predicates + .iter() + .enumerate() + .filter(|(idx, _)| !expression.is_switched_off(*idx)) + .map(|(_, plan)| plan), rows.clone(), &selected.mask, )?; @@ -260,7 +306,7 @@ impl Planner for FilterPlanner { return Ok(PlannerOutput::NeedsIO(batch)); } } - if let Some((index, adaptor)) = &mut self.active { + if let Some((index, version, adaptor)) = &mut self.active { if adaptor.state() != State::Done { return Ok(match adaptor.compute()? { AdaptorStep::Yield => PlannerOutput::Continue, @@ -283,9 +329,15 @@ impl Planner for FilterPlanner { conjunct = *index, "v2 filter conjunct evaluated" ); + let selectivity = selected.mask.true_count() as f64 / input as f64; self.filter .expression - .report_selectivity(*index, selected.mask.true_count() as f64 / input as f64); + .report_selectivity(*index, selectivity); + if let Some(version) = version { + self.filter + .expression + .report_dynamic_selectivity(*index, *version, selectivity); + } self.active = None; self.pieces.clear(); } @@ -293,8 +345,24 @@ impl Planner for FilterPlanner { self.selected = None; return Ok(PlannerOutput::Done); } - if let Some(index) = self.filter.expression.next_conjunct(&self.remaining) { + while let Some(index) = self.filter.expression.next_conjunct(&self.remaining) { self.remaining.set(index, false); + // Sampled before evaluating, so an update during evaluation is not credited to it. + let version = self.filter.expression.dynamic_version(index); + if let Some(version) = version + && self + .filter + .expression + .skips_dynamic_conjunct(index, version) + { + tracing::debug!( + row_start = selected.scope.rows.start, + row_end = selected.scope.rows.end, + conjunct = index, + "v2 filter conjunct switched off" + ); + continue; + } let scan = Scan::try_new( self.scan.session.clone(), self.filter.predicates[index].clone(), @@ -307,6 +375,7 @@ impl Planner for FilterPlanner { .with_value_cache(self.scan.values.clone()); self.active = Some(( index, + version, ScanAdaptor::new(scan, Arc::clone(&self.scan.locations)), )); return Ok(PlannerOutput::Continue); diff --git a/vortex-layout/src/scan/execution/prune.rs b/vortex-layout/src/scan/execution/prune.rs index 325f5f897d7..93808dea22b 100644 --- a/vortex-layout/src/scan/execution/prune.rs +++ b/vortex-layout/src/scan/execution/prune.rs @@ -63,7 +63,7 @@ impl PrunePlanner { let pruning: Vec<_> = scan .filter .iter() - .flat_map(|filter| filter.pruning().cloned()) + .flat_map(|filter| filter.pruning_over(rows.end - rows.start).cloned()) .collect(); let len = usize::try_from(rows.end - rows.start).unwrap_or(usize::MAX); Self { diff --git a/vortex-layout/src/scan/filter.rs b/vortex-layout/src/scan/filter.rs index 2cb36068913..90fd486b344 100644 --- a/vortex-layout/src/scan/filter.rs +++ b/vortex-layout/src/scan/filter.rs @@ -5,6 +5,7 @@ use std::iter; use bit_vec::BitVec; use itertools::Itertools; +use parking_lot::Mutex; use parking_lot::RwLock; use sketches_ddsketch::DDSketch; use vortex_array::expr::BoundExpression; @@ -18,6 +19,13 @@ use vortex_error::vortex_panic; /// The selectivity histogram quantile to use for reordering conjuncts. Where 0 == no rows match. const DEFAULT_SELECTIVITY_QUANTILE: f64 = 0.1; +/// A dynamic conjunct keeping at least this fraction of its input rows is considered unselective. +const UNSELECTIVE_DYNAMIC_SELECTIVITY: f64 = 0.99; + +/// Consecutive unselective evaluations, at one dynamic version, after which a skippable dynamic +/// conjunct stops being evaluated until its dynamic values change. +pub(crate) const UNSELECTIVE_DYNAMIC_SPLITS: u32 = 4; + /// A [`FilterExpr`] splits boolean expressions into individual conjunctions, tracks /// statistics about selectivity, and uses this information to reorder the evaluation of the /// conjunctions in an attempt to minimize the work done. @@ -32,6 +40,9 @@ pub struct FilterExpr { ordering: RwLock>, /// The quantile to use from the selectivity histogram of each conjunct. selectivity_quantile: f64, + /// For each conjunct, the dynamic version and how many consecutive evaluations at that version + /// kept almost every row. `None` unless unselective dynamic conjuncts may be skipped. + unselective_dynamic: Option>>, } fn bound_conjuncts(expr: &BoundExpression) -> Vec { @@ -70,6 +81,68 @@ impl FilterExpr { // comparison operator to perform. e.g. == might be more selective than <=? Not obvious. ordering: RwLock::new((0..num_conjuncts).collect()), selectivity_quantile: DEFAULT_SELECTIVITY_QUANTILE, + unselective_dynamic: None, + } + } + + /// Allows skipping dynamic conjuncts that keep almost every row. + /// + /// Once a conjunct containing dynamic expressions has kept almost every row for several + /// consecutive evaluations, it is not evaluated again until its dynamic values change. The scan + /// may then return rows the conjunct would reject, so only enable this when dynamic + /// expressions are optimization hints whose producer applies them again. + pub fn with_skip_unselective_dynamic(mut self, skip: bool) -> Self { + self.unselective_dynamic = skip.then(|| { + iter::repeat_with(|| Mutex::new((0, 0))) + .take(self.conjuncts.len()) + .collect() + }); + self + } + + /// Whether unselective dynamic conjuncts may be skipped. + #[inline] + pub fn skips_unselective_dynamic(&self) -> bool { + self.unselective_dynamic.is_some() + } + + /// The current dynamic version of the conjunct, or `None` when it has no dynamic expressions. + #[inline] + pub fn dynamic_version(&self, conjunct_idx: usize) -> Option { + self.dynamic_updates(conjunct_idx) + .map(DynamicExprUpdates::version) + } + + /// Whether the dynamic conjunct can be skipped at dynamic `version`, because it kept almost + /// every row for its last evaluations at that version. + pub fn skips_dynamic_conjunct(&self, conjunct_idx: usize, version: u64) -> bool { + self.unselective_dynamic.as_ref().is_some_and(|states| { + let (state_version, streak) = *states[conjunct_idx].lock(); + state_version == version && streak >= UNSELECTIVE_DYNAMIC_SPLITS + }) + } + + /// Whether the conjunct is a dynamic conjunct that is currently switched off. + pub fn is_switched_off(&self, conjunct_idx: usize) -> bool { + self.unselective_dynamic.is_some() + && self + .dynamic_version(conjunct_idx) + .is_some_and(|version| self.skips_dynamic_conjunct(conjunct_idx, version)) + } + + /// Records the selectivity of a dynamic conjunct evaluated at dynamic `version`. + pub fn report_dynamic_selectivity(&self, conjunct_idx: usize, version: u64, selectivity: f64) { + let Some(states) = &self.unselective_dynamic else { + return; + }; + let mut state = states[conjunct_idx].lock(); + if state.0 != version { + *state = (version, 0); + } + if selectivity >= UNSELECTIVE_DYNAMIC_SELECTIVITY { + state.1 = state.1.saturating_add(1); + } else { + state.1 = 0; } } @@ -168,6 +241,7 @@ mod tests { use vortex_error::VortexResult; use super::FilterExpr; + use super::UNSELECTIVE_DYNAMIC_SPLITS; #[test] fn bound_conjuncts_preserve_order_and_types() -> VortexResult<()> { @@ -210,4 +284,49 @@ mod tests { assert_eq!(*filter.ordering.read(), vec![1, 0]); Ok(()) } + + #[test] + fn skips_unselective_dynamic_conjunct_until_version_changes() -> VortexResult<()> { + let dtype = DType::Bool(Nullability::Nullable); + let filter = FilterExpr::new(and(root(), not(root())).bind(&dtype)?) + .with_skip_unselective_dynamic(true); + + for _ in 0..UNSELECTIVE_DYNAMIC_SPLITS { + assert!(!filter.skips_dynamic_conjunct(0, 1)); + filter.report_dynamic_selectivity(0, 1, 1.0); + } + assert!(filter.skips_dynamic_conjunct(0, 1)); + assert!(!filter.skips_dynamic_conjunct(1, 1)); + // New dynamic values must be evaluated again. + assert!(!filter.skips_dynamic_conjunct(0, 2)); + filter.report_dynamic_selectivity(0, 2, 1.0); + assert!(!filter.skips_dynamic_conjunct(0, 2)); + Ok(()) + } + + #[test] + fn selective_evaluation_resets_unselective_streak() -> VortexResult<()> { + let dtype = DType::Bool(Nullability::Nullable); + let filter = FilterExpr::new(root().bind(&dtype)?).with_skip_unselective_dynamic(true); + + for _ in 1..UNSELECTIVE_DYNAMIC_SPLITS { + filter.report_dynamic_selectivity(0, 1, 1.0); + } + filter.report_dynamic_selectivity(0, 1, 0.5); + filter.report_dynamic_selectivity(0, 1, 1.0); + assert!(!filter.skips_dynamic_conjunct(0, 1)); + Ok(()) + } + + #[test] + fn never_skips_without_opt_in() -> VortexResult<()> { + let dtype = DType::Bool(Nullability::Nullable); + let filter = FilterExpr::new(root().bind(&dtype)?); + for _ in 0..=UNSELECTIVE_DYNAMIC_SPLITS { + filter.report_dynamic_selectivity(0, 1, 1.0); + } + assert!(!filter.skips_dynamic_conjunct(0, 1)); + assert!(!filter.is_switched_off(0)); + Ok(()) + } } diff --git a/vortex-layout/src/scan/v2/mod.rs b/vortex-layout/src/scan/v2/mod.rs index 77933bc4126..18acf916708 100644 --- a/vortex-layout/src/scan/v2/mod.rs +++ b/vortex-layout/src/scan/v2/mod.rs @@ -99,6 +99,7 @@ pub struct ScanBuilder { row_range: Option>, selection: Selection, row_offset: u64, + skip_unselective_dynamic: bool, } impl ScanBuilder { @@ -112,6 +113,7 @@ impl ScanBuilder { row_range: None, selection: Selection::default(), row_offset: 0, + skip_unselective_dynamic: false, } } @@ -137,6 +139,18 @@ impl ScanBuilder { self } + /// Allows the scan to switch off a filter conjunct containing dynamic expressions once it has + /// kept almost every row on several consecutive chunks, skipping its zone pruning and its + /// evaluation until its dynamic values change. + /// + /// The scan may then return rows the conjunct would reject, so only enable this when the + /// dynamic expressions are hints whose producer applies them again, such as DataFusion's + /// TopK and hash-join dynamic filters. Off by default. + pub fn with_skip_unselective_dynamic(mut self, skip: bool) -> Self { + self.skip_unselective_dynamic = skip; + self + } + /// Restricts scanning to a contiguous file-local row range. pub fn with_row_range(mut self, row_range: Range) -> Self { self.row_range = Some(row_range); @@ -179,11 +193,23 @@ impl ScanBuilder { let filter = self .filter .map(|filter| -> VortexResult<_> { + let skip = self.skip_unselective_dynamic; match cache.filter_plans(&filter) { - Some(plans) => Ok(plans), + Some(plans) if plans.skips_unselective_dynamic() == skip => Ok(plans), + // A scan whose skipping differs from the cached plans' plans its own. + Some(_) => Ok(Arc::new(FilterPlans::new( + filter, + &root, + &self.session, + skip, + )?)), None => { - let plans = - Arc::new(FilterPlans::new(filter.clone(), &root, &self.session)?); + let plans = Arc::new(FilterPlans::new( + filter.clone(), + &root, + &self.session, + skip, + )?); Ok(cache.insert_filter_plans(filter, plans)) } } diff --git a/vortex-layout/src/scan/v2/tests.rs b/vortex-layout/src/scan/v2/tests.rs index b80a0479b88..ea88dd5ba40 100644 --- a/vortex-layout/src/scan/v2/tests.rs +++ b/vortex-layout/src/scan/v2/tests.rs @@ -124,10 +124,19 @@ fn table_chunk(start: usize) -> VortexResult { async fn write( session: &VortexSession, strategy: &dyn LayoutStrategy, +) -> VortexResult<(Arc, LayoutRef)> { + write_chunks(session, strategy, CHUNKS).await +} + +/// Writes `count` table chunks with `strategy`. +async fn write_chunks( + session: &VortexSession, + strategy: &dyn LayoutStrategy, + count: usize, ) -> VortexResult<(Arc, LayoutRef)> { let segments = Arc::new(TestSegments::default()); let (mut sequence_id, eof) = SequenceId::root().split(); - let chunks = (0..CHUNKS) + let chunks = (0..count) .map(|chunk| Ok((sequence_id.advance(), table_chunk(chunk * CHUNK_ROWS)?))) .collect::>(); let dtype = table_chunk(0)?.dtype().clone(); @@ -940,21 +949,21 @@ async fn prune(scan: &Arc, rows: Range) -> VortexResult VortexResult<()> { +/// A scan whose filter is `ints > bound`, a dynamic comparison, over `chunks` table chunks whose +/// segment reads are counted. +async fn dynamic_scan( + chunks: usize, + bound: &Arc, +) -> VortexResult<(Arc, Arc)> { let session = new_session().with_tokio(); - let (segments, layout) = write(&session, zoned_table().as_ref()).await?; + let (segments, layout) = write_chunks(&session, zoned_table().as_ref(), chunks).await?; let counted = Arc::new(CountingSegmentSource::new(segments)); let segments = Arc::clone(&counted) as Arc; let mut file = scan_file(&segments, &layout)?; file.cache = Some(ScanCache::with_retained_bytes(0)); - let bound = Arc::new(AtomicI32::new(-1)); let ints = get_item("ints", root()); let filter = { - let bound = Arc::clone(&bound); + let bound = Arc::clone(bound); dynamic( CompareOperator::Gt, move || Some(bound.load(Ordering::Relaxed).into()), @@ -963,24 +972,157 @@ async fn pruning_again_reuses_the_first_reads() -> VortexResult<()> { ints, ) }; - let scan = Arc::new( - ScanBuilder::new(session.clone(), file) - .with_some_filter(Some(filter.bind(layout.dtype())?)) - .prepare()?, - ); + let scan = ScanBuilder::new(session, file) + .with_some_filter(Some(filter.bind(layout.dtype())?)) + .prepare()?; + Ok((Arc::new(scan), counted)) +} + +/// Pruning some of a scan's rows again, as an engine does once a dynamic filter has tightened, +/// prunes with the tightened value from the zone maps the first pruning read: it reads no segment, +/// even when nothing is retained. +#[tokio::test] +async fn pruning_again_reuses_the_first_reads() -> VortexResult<()> { + // Both prunings cover more rows than one filtered chunk, so neither skips the dynamic plan. + let bound = Arc::new(AtomicI32::new(-1)); + let (scan, counted) = dynamic_scan(200, &bound).await?; let all = prune(&scan, scan.rows().clone()).await?; let reads = counted.requests(); assert!(reads > 0, "the first pruning reads the zone maps"); - assert_eq!(all.map(|kept| kept.true_count()), Some(4_000)); + assert_eq!(all.map(|kept| kept.true_count()), Some(200_000)); bound.store(2_999, Ordering::Relaxed); - let part = prune(&scan, 1_000..4_000).await?; - assert_eq!(part.map(|kept| kept.true_count()), Some(1_000)); + let part = prune(&scan, 1_000..151_000).await?; + assert_eq!(part.map(|kept| kept.true_count()), Some(148_000)); assert_eq!(counted.requests(), reads); Ok(()) } +/// A scan of at most one filtered chunk skips the pruning plan of a dynamic conjunct and reads no +/// zone map: its chunk evaluates the conjunct next. +#[tokio::test] +async fn small_scans_skip_dynamic_pruning() -> VortexResult<()> { + let bound = Arc::new(AtomicI32::new(2_999)); + let (scan, counted) = dynamic_scan(CHUNKS, &bound).await?; + + assert!(prune(&scan, scan.rows().clone()).await?.is_none()); + assert_eq!(counted.requests(), 0); + Ok(()) +} + +/// Runs a scan of the `strings` column of `TABLE_CHUNKS` table chunks, filtered on `ints > bound` +/// with a dynamic comparison when there is a bound, over `counted`. Returns the rows it kept and +/// the segment requests it made. +async fn strings_scan( + session: &VortexSession, + counted: &Arc, + layout: &LayoutRef, + file: ScanFile, + filter: Option<&Expression>, + skip_unselective_dynamic: bool, +) -> VortexResult<(Arc, usize, usize)> { + let projection = get_item("strings", root()).bind(layout.dtype())?; + let scan = Arc::new( + ScanBuilder::new(session.clone(), file) + .with_projection(projection) + .with_some_filter(filter.map(|f| f.bind(layout.dtype())).transpose()?) + .with_skip_unselective_dynamic(skip_unselective_dynamic) + .prepare()?, + ); + let before = counted.requests(); + let rows = run_rows(&scan, scan.rows().clone()) + .await? + .iter() + .map(|array| array.len()) + .sum(); + Ok((scan, rows, counted.requests() - before)) +} + +/// A dynamic conjunct that keeps every row on enough chunks is switched off: later scans neither +/// prune its zones nor read its column, so they read no more than an unfiltered scan. A new dynamic +/// value switches it back on. Without the opt-in it keeps being evaluated. +#[rstest] +#[case::opted_in(true)] +#[case::default(false)] +#[tokio::test] +async fn unselective_dynamic_conjuncts_switch_off_until_their_value_changes( + #[case] skip: bool, +) -> VortexResult<()> { + // Five filtered chunks per scan. + const TABLE_CHUNKS: usize = 600; + let session = new_session().with_tokio(); + let (segments, layout) = write_chunks(&session, zoned_table().as_ref(), TABLE_CHUNKS).await?; + let counted = Arc::new(CountingSegmentSource::new(segments)); + let segments = Arc::clone(&counted) as Arc; + let file = || -> VortexResult { + let mut file = scan_file(&segments, &layout)?; + file.cache = Some(ScanCache::with_retained_bytes(0)); + Ok(file) + }; + let shared = file()?; + + let (_, all, unfiltered_reads) = + strings_scan(&session, &counted, &layout, file()?, None, skip).await?; + assert_eq!(all, TABLE_CHUNKS * CHUNK_ROWS); + + let bound = Arc::new(AtomicI32::new(-1)); + let ints = get_item("ints", root()); + let filter = { + let bound = Arc::clone(&bound); + dynamic( + CompareOperator::Gt, + move || Some(bound.load(Ordering::Relaxed).into()), + ints.return_dtype(layout.dtype())?, + true, + ints, + ) + }; + + // The first scan evaluates the conjunct on every chunk, which keeps every row. + let (scan, rows, first_reads) = strings_scan( + &session, + &counted, + &layout, + shared.clone(), + Some(&filter), + skip, + ) + .await?; + assert_eq!(rows, all); + assert!(first_reads > unfiltered_reads); + let plans = scan.filter().expect("the scan has a filter"); + assert_eq!(plans.is_switched_off(0), skip); + + let (_, rows, reads) = strings_scan( + &session, + &counted, + &layout, + shared.clone(), + Some(&filter), + skip, + ) + .await?; + assert_eq!(rows, all); + // Filtered and unfiltered scans cut chunks differently, so a switched-off scan can read a few + // segments fewer than an unfiltered one, but never the filtered column. + if skip { + assert!(reads <= unfiltered_reads, "{reads} > {unfiltered_reads}"); + } else { + assert!(reads > unfiltered_reads); + } + + // A tightened bound is a new dynamic value, so the conjunct is evaluated again. It now + // rejects half the rows, so it stays switched on. + bound.store(299_999, Ordering::Relaxed); + let (scan, rows, _) = + strings_scan(&session, &counted, &layout, shared, Some(&filter), skip).await?; + assert_eq!(rows, all - 300_000); + let plans = scan.filter().expect("the scan has a filter"); + assert!(!plans.is_switched_off(0)); + Ok(()) +} + /// Scans of one file that share a cache share their filter plans, so the pruning plans run once: /// the second scan with the same filter re-reads the data it needs but never the zone maps. Each /// scan binds its own filter and projection, as each partition of an engine does. From 3be54de2d4cdf014ddca45508c9ff0bfec76740b Mon Sep 17 00:00:00 2001 From: Robert Kruszewski Date: Wed, 4 Nov 2026 17:52:02 +0000 Subject: [PATCH 32/41] Speed up RunEnd filter with a rank directory and POPCNT dispatch (#10575) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Speeds up the run-preserving path of the RunEnd filter kernel, most for short runs. The old loop counted the selected rows in each run with a separate `BitBuffer::count_range` call, and for short runs that call's setup cost more than the counting. ## Changes - **Rank lookup instead of range counts.** A filtered run end is the number of selected rows before the source run end. A small table of 64-bit mask words with running popcounts answers that with one load and one popcount per run. - **One pass.** The loop writes each new end at the current output position and moves forward only when the run has selected rows, so the ends come out compacted with no second pass. - **POPCNT through `fearless_simd`.** The x86-64 baseline has no `popcnt`, so the loop is dispatched with `dispatch!(Level::new(), …)`. Each level gets its own copy, which also lets `BitBuffer::collect_bool` inline. --------- Signed-off-by: Robert Kruszewski Co-authored-by: Claude Opus 5.5 --- vortex-array/benches/run_end_filter.rs | 3 +- .../src/arrays/runend/compute/filter.rs | 251 +++++++++++++----- 2 files changed, 186 insertions(+), 68 deletions(-) diff --git a/vortex-array/benches/run_end_filter.rs b/vortex-array/benches/run_end_filter.rs index 45d05b01679..b72fd60c8b1 100644 --- a/vortex-array/benches/run_end_filter.rs +++ b/vortex-array/benches/run_end_filter.rs @@ -5,8 +5,7 @@ //! //! This measures the kernel directly rather than going through the lazy //! `ArrayRef::filter` (which only builds a `FilterArray` node and does not run -//! the kernel). The hot work is a per-run popcount of the predicate mask, which -//! now uses `BitBuffer::count_range` (SIMD) instead of a bit-by-bit walk. +//! the kernel). The hot work is a rank of the predicate mask at every run end. #![expect(clippy::cast_possible_truncation)] #![expect(clippy::cast_precision_loss)] diff --git a/vortex-array/src/arrays/runend/compute/filter.rs b/vortex-array/src/arrays/runend/compute/filter.rs index 172908051ff..ded60466cd8 100644 --- a/vortex-array/src/arrays/runend/compute/filter.rs +++ b/vortex-array/src/arrays/runend/compute/filter.rs @@ -1,17 +1,49 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +//! Filtering run-end encoded arrays. +//! +//! Small or sparse selections use direct take. Otherwise, filtering updates the run ends and +//! removes runs that contain no selected rows, preserving the run-end encoding. +//! +//! A run's new end is the number of selected rows before its original end, called the mask's +//! _rank_. A run disappears when this count does not increase: +//! +//! ```text +//! Input rows: A A A | B B | C C C +//! Input ends: 3 5 8 +//! Selection mask: 1 0 1 | 0 0 | 1 0 1 +//! Rank at each end: 2 2 4 +//! +//! Output ends: [2, 4] +//! Output values: [A, C] +//! Output rows: A A C C +//! ``` +//! +//! Run ends never decrease, so one walk over the mask's 64-bit words answers every rank query. +//! The walk keeps the number of selected rows in the words it has passed, and a query adds a +//! population count of the relevant bits within the current word. This avoids a separate range +//! count for every run, which is especially useful for short runs, and it builds no index, so +//! long runs cost no more than one pass over the mask. +//! +//! Each query still needs a population count, so the instructions used for counting matter. +//! [`fearless_simd`] selects a version of the loop compiled for the current CPU's features. +//! On supported x86 CPUs, this enables POPCNT, which counts the set bits in a word with one +//! instruction. The rank queries themselves operate on scalar words rather than vectors. + use std::cmp::min; -use std::ops::AddAssign; +use fearless_simd::Level; +use fearless_simd::Simd; +use fearless_simd::dispatch; +use fearless_simd_macros::simd; use num_traits::AsPrimitive; -use num_traits::NumCast; use vortex_buffer::BitBuffer; use vortex_buffer::BitChunkIterator; -use vortex_buffer::BufferAllocatorRef; use vortex_buffer::BufferMut; use vortex_error::VortexExpect; use vortex_error::VortexResult; +use vortex_error::vortex_ensure_eq; use vortex_mask::Mask; use crate::ArrayRef; @@ -26,6 +58,7 @@ use crate::arrays::runend::array::RunEndArraySlotsExt; use crate::arrays::runend::compute::take::take_indices_unchecked; use crate::dtype::NativePType; use crate::match_each_unsigned_integer_ptype; +use crate::memory::BufferAllocatorRef; use crate::validity::Validity; /// Takes directly below this average number of selected rows per source run. @@ -100,69 +133,134 @@ impl FilterKernel for RunEnd { /// exactly `length` bits. The returned ends are strictly increasing and contain one entry for each /// selected run value. /// -/// Adapted from the [Apache Arrow Rust implementation](https://github.com/apache/arrow-rs/blob/b1f5c250ebb6c1252b4e7c51d15b8e77f4c361fa/arrow-select/src/filter.rs#L425). -pub fn filter_run_end_primitive + AsPrimitive>( +/// A filtered run end is the rank of the mask at the source run end, i.e. the number of selected +/// rows before it, and a run is retained when its rank exceeds the previous run's rank. +pub fn filter_run_end_primitive>( run_ends: &[R], offset: u64, length: u64, mask: &BitBuffer, allocator: &BufferAllocatorRef, -) -> VortexResult<(PrimitiveArray, Mask)> { - let mut filtered_run_ends = BufferMut::::zeroed_in(run_ends.len(), allocator.clone()); +) -> VortexResult<(PrimitiveArray, Mask)> +where + u64: AsPrimitive, +{ + // The rank loop reads the mask without bounds checks and relies on this length. + vortex_ensure_eq!(mask.len() as u64, length); - let mut retained_run_count = 0; - let mut filtered_end = R::zero(); + let (filtered_run_ends, retained_runs) = + run_end_ranks(run_ends, offset, length, mask, allocator); + + Ok(( + PrimitiveArray::new(filtered_run_ends, Validity::NonNullable), + Mask::from(retained_runs), + )) +} + +/// Returns the rank of `mask` at every retained source run end and the runs that contain selected +/// rows. `mask` must contain exactly `length` bits. +fn run_end_ranks>( + run_ends: &[R], + offset: u64, + length: u64, + mask: &BitBuffer, + allocator: &BufferAllocatorRef, +) -> (BufferMut, BitBuffer) +where + u64: AsPrimitive, +{ + // The x86-64 baseline has no POPCNT, so a plain build lowers every per-run popcount to a + // bit-twiddling sequence that dominates the loop. Levels from SSE4.2 up enable POPCNT. + dispatch!(Level::new(), simd => { + run_end_ranks_simd(simd, run_ends, offset, length, mask, allocator) + }) +} + +/// Rank loop compiled with the target features of `simd`. +/// +/// Each level has its own instantiation, so each `BitBuffer::collect_bool` instantiation has one +/// caller and LLVM inlines it into the feature-enabled body. An out-of-line `collect_bool` would +/// compile the predicate without POPCNT. +/// +/// The loop writes every rank at the current retained count and advances the count only for a +/// retained run, so it compacts the ends without a branch or a second pass. +#[simd] +fn run_end_ranks_simd>( + _: S, + run_ends: &[R], + offset: u64, + length: u64, + mask: &BitBuffer, + allocator: &BufferAllocatorRef, +) -> (BufferMut, BitBuffer) +where + u64: AsPrimitive, +{ let mut rank = MaskRank::new(mask); - let mut selected_before_run = 0; - - let values_mask: Mask = BitBuffer::collect_bool(run_ends.len(), |run_idx| { - let run_end = min(run_ends[run_idx].as_() - offset, length); - let run_end_idx = run_end - .try_into() - .vortex_expect("run end index must fit in usize"); - // Run ends increase, so the rank walks the mask's words once over all runs instead of - // counting each run's range from scratch, which dominates when runs are short. - let selected_through_run = rank.rank(run_end_idx); - let selected_in_run = selected_through_run - selected_before_run; - selected_before_run = selected_through_run; - filtered_end += ::from(selected_in_run) - .vortex_expect("run popcount must fit in run-end native type"); - let retain_run = selected_in_run > 0; - - // Always write the current end, then advance only for a retained run. This keeps the loop - // branchless. - filtered_run_ends[retained_run_count] = filtered_end; - retained_run_count += retain_run as usize; + let mut filtered_run_ends = BufferMut::::with_capacity_in(run_ends.len(), allocator.clone()); + let filtered_run_ends_out = filtered_run_ends.spare_capacity_mut(); + let mut retained_run_count = 0; + let mut prev_rank = 0u64; + + let retained_runs = BitBuffer::collect_bool(run_ends.len(), |run_idx| { + // SAFETY: `collect_bool` calls this closure with `run_idx < run_ends.len()`. + let run_end = unsafe { *run_ends.get_unchecked(run_idx) }; + + let run_end: u64 = run_end.as_(); + let position = >::as_(min(run_end - offset, length)); + // The clamp keeps the position at most `length`, which is the mask length, and run ends + // never decrease, so the positions never decrease. + let run_rank: u64 = rank.rank(position).as_(); + + // SAFETY: `retained_run_count <= run_idx`, and the spare capacity holds `run_ends.len()` + // elements. The rank is at most `length`, which fits in `R` because the source run ends + // reach `offset + length`. + unsafe { filtered_run_ends_out.get_unchecked_mut(retained_run_count) } + .write(run_rank.as_()); + + let retain_run = run_rank > prev_rank; + retained_run_count += usize::from(retain_run); + prev_rank = run_rank; retain_run - }) - .into(); + }); - filtered_run_ends.truncate(retained_run_count); + // SAFETY: the loop initialized the first `retained_run_count` elements. + unsafe { filtered_run_ends.set_len(retained_run_count) }; - Ok(( - PrimitiveArray::new(filtered_run_ends, Validity::NonNullable), - values_mask, - )) + (filtered_run_ends, retained_runs) } /// Counts the set bits of a mask before non-decreasing positions, reading each 64-bit word once. +/// +/// The methods are always inlined, so the rank loop counts with the POPCNT of its dispatch level. struct MaskRank<'a> { words: BitChunkIterator<'a>, + + /// The last, partial word of the mask. It is zero when the mask length is a multiple of 64. remainder: u64, + /// The index of `word` among the mask's words. word_idx: usize, + word: u64, + /// The set bits in the words before `word`. before_word: usize, } impl<'a> MaskRank<'a> { + #[expect( + clippy::inline_always, + reason = "must inline into the dispatch body to use its POPCNT" + )] + #[inline(always)] fn new(mask: &'a BitBuffer) -> Self { let chunks = mask.chunks(); let remainder = chunks.remainder_bits(); let mut words = chunks.iter(); let word = words.next().unwrap_or(remainder); + Self { words, remainder, @@ -172,8 +270,13 @@ impl<'a> MaskRank<'a> { } } - /// The set bits in `0..pos`, for `pos` no smaller than in the previous call. - #[inline] + /// Returns the set bits in `0..pos`, for `pos` at most the mask length and no smaller than in + /// the previous call. + #[expect( + clippy::inline_always, + reason = "must inline into the dispatch body to use its POPCNT" + )] + #[inline(always)] fn rank(&mut self, pos: usize) -> usize { let word_idx = pos / 64; while self.word_idx < word_idx { @@ -181,13 +284,9 @@ impl<'a> MaskRank<'a> { self.word_idx += 1; self.word = self.words.next().unwrap_or(self.remainder); } - let bits = pos % 64; - let low = if bits == 0 { - 0 - } else { - u64::MAX >> (64 - bits) - }; - self.before_word + (self.word & low).count_ones() as usize + + let low_bits = (1u64 << (pos % 64)) - 1; + self.before_word + (self.word & low_bits).count_ones() as usize } } @@ -198,12 +297,12 @@ mod tests { use vortex_error::VortexResult; use vortex_mask::Mask; + use super::filter_run_end_primitive; use crate::IntoArray; use crate::VortexSessionExecute; use crate::arrays::PrimitiveArray; use crate::arrays::runend::RunEnd; use crate::arrays::runend::RunEndArray; - use crate::arrays::runend::compute::filter::filter_run_end_primitive; use crate::arrays::runend::tests::SESSION; use crate::assert_arrays_eq; @@ -265,46 +364,66 @@ mod tests { Ok(()) } - /// The rank walk must count the same rows per run as counting each run's range, for masks - /// that start inside a byte and end inside or at the end of a word. + /// The rank walk must count the same rows per run as counting each run's range. The cases + /// cover masks that start inside a byte and end inside or at the end of a word, and sliced + /// arrays whose first run starts before the slice and whose last run ends after it. #[rstest] - #[case(0, 128)] - #[case(3, 128)] - #[case(5, 200)] - #[case(61, 64)] - #[case(7, 1)] - fn short_runs_count_each_runs_selected_rows( + #[case(0, 0, 128)] + #[case(0, 3, 128)] + #[case(0, 5, 200)] + #[case(0, 61, 64)] + #[case(0, 7, 1)] + #[case(3, 5, 200)] + #[case(70, 0, 129)] + fn filter_run_end_primitive_counts_each_runs_selected_rows( + #[case] offset: u64, #[case] mask_offset: usize, #[case] len: usize, ) -> VortexResult<()> { let bits = BitBuffer::from_iter((0..mask_offset + len).map(|i| i % 3 != 0 || i % 7 == 0)); let mask = bits.slice(mask_offset..mask_offset + len); - // Runs of 1, 2 and 3 rows. + + // Runs of 1, 2, 3 and 150 rows, so the walk also skips several words in one query. + let slice_end = offset + len as u64; let mut run_ends = Vec::new(); - let mut end = 0u32; - let mut run_len = 1; - while (end as usize) < len { - end = (end + run_len).min(u32::try_from(len)?); - run_ends.push(end); - run_len = run_len % 3 + 1; + let mut end = 0u64; + for run_len in [1, 2, 3, 150].into_iter().cycle() { + end += run_len; + if end > offset { + run_ends.push(u32::try_from(end)?); + } + if end >= slice_end { + break; + } } - let ctx = SESSION.create_execution_ctx(); - let (filtered_ends, values_mask) = - filter_run_end_primitive(&run_ends, 0, len as u64, &mask, ctx.allocator())?; + // End the last run after the slice, as in a slice of a longer array. + if let Some(last) = run_ends.last_mut() { + *last += 5; + } + + let (filtered_ends, values_mask) = filter_run_end_primitive( + &run_ends, + offset, + len as u64, + &mask, + SESSION.create_execution_ctx().allocator(), + )?; let mut expected_ends = Vec::new(); let mut expected_values = Vec::new(); let (mut start, mut selected) = (0, 0u32); for &end in &run_ends { - let in_run = mask.count_range(start, end as usize); + let end = usize::try_from(u64::from(end) - offset)?.min(len); + let in_run = mask.count_range(start, end); selected += u32::try_from(in_run)?; expected_values.push(in_run > 0); if in_run > 0 { expected_ends.push(selected); } - start = end as usize; + start = end; } + assert_eq!(filtered_ends.as_slice::(), expected_ends.as_slice()); assert_eq!(values_mask, Mask::from_iter(expected_values)); Ok(()) From 9d2da798b29be7571b63f0d185708d08ccf1f184 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 13:00:36 -0500 Subject: [PATCH 33/41] Fix the mixed-dimension chunk test in vortex-bench (#11236) ## Summary `chunked_input_with_mixed_dimensions_returns_error` has failed on `develop` since #10080 removed the "Mismatched types" text that it looks for. CI excludes `vortex-bench` from nextest, so CI never ran it. The conversion still rejects chunks with different vector dimensions at the `vortex_ensure_eq!` in `list_to_vector_ext`, so only the assertion was stale. ## Changes The test now checks that the error names both vector dtypes: dimension 2 from the second chunk against dimension 3 from the first. Signed-off-by: Connor Tsui --- vortex-bench/src/vector_dataset/convert.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vortex-bench/src/vector_dataset/convert.rs b/vortex-bench/src/vector_dataset/convert.rs index 1b7c9a45ea5..5358f4411f7 100644 --- a/vortex-bench/src/vector_dataset/convert.rs +++ b/vortex-bench/src/vector_dataset/convert.rs @@ -540,6 +540,10 @@ mod tests { .into_array(); let err = list_to_vector_ext(chunked).unwrap_err().to_string(); - assert!(err.contains("Mismatched types"), "unexpected error: {err}"); + assert!( + err.contains("left: vortex.tensor.vector(fixed_size_list(f32)[2])") + && err.contains("right: vortex.tensor.vector(fixed_size_list(f32)[3])"), + "unexpected error: {err}" + ); } } From 4addfbaa126229da904c30e9d7cd5bd156624263 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 13:37:54 -0500 Subject: [PATCH 34/41] Fix the return dtype of grouped is_sorted and is_constant (#11238) ## Summary Every grouped `is_sorted` and `is_constant` call fails with "Return DType mismatch". Both `finalize` methods read the result field out of the nullable partial struct, which gives `Bool?` while the return dtype is a non-nullable `Bool`. #11188 found the `is_sorted` case, and its grouped pushdown test covers only `min_max` because of it. ## Changes Both `finalize` methods now fill null partials. A null partial comes from an empty or null group and is the empty state, so each fill uses the verdict that `finalize_scalar` already gives that state: `true` for `is_sorted` (vacuously sorted) and `false` for `is_constant`. Grouped and scalar results now agree. Note that a null group gets the same verdict as an empty group, since the partial can't tell them apart and the return dtype is non-nullable. Each aggregate gets a grouped test that puts empty, all-null, and null groups next to ordinary ones. --------- Signed-off-by: Connor Tsui --- .../src/aggregate_fn/fns/is_constant/mod.rs | 37 ++++++++++++++++++- .../src/aggregate_fn/fns/is_sorted/mod.rs | 36 +++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/vortex-array/src/aggregate_fn/fns/is_constant/mod.rs b/vortex-array/src/aggregate_fn/fns/is_constant/mod.rs index 0c1b2c40ce6..24efc6c4ef4 100644 --- a/vortex-array/src/aggregate_fn/fns/is_constant/mod.rs +++ b/vortex-array/src/aggregate_fn/fns/is_constant/mod.rs @@ -459,7 +459,10 @@ impl AggregateFnVTable for IsConstant { _args: AggregateArgs<'_, Self::Options>, partials: ArrayRef, ) -> VortexResult { - partials.get_item(NAMES.get(0).vortex_expect("out of bounds").clone()) + // A null partial is the empty state of an empty or null group, which is not constant. + partials + .get_item(NAMES.get(0).vortex_expect("out of bounds").clone())? + .fill_null(false) } fn finalize_scalar( @@ -491,7 +494,9 @@ mod tests { use crate::aggregate_fn::AggregateDTypes; use crate::aggregate_fn::AggregateFnVTable; use crate::aggregate_fn::DynAccumulator; + use crate::aggregate_fn::DynGroupedAccumulator; use crate::aggregate_fn::EmptyOptions; + use crate::aggregate_fn::GroupedAccumulator; use crate::aggregate_fn::fns::is_constant::IsConstant; use crate::aggregate_fn::fns::is_constant::IsConstantPartial; use crate::aggregate_fn::fns::is_constant::is_constant; @@ -501,9 +506,11 @@ mod tests { use crate::arrays::DecimalArray; use crate::arrays::DictArray; use crate::arrays::ListArray; + use crate::arrays::ListViewArray; use crate::arrays::PrimitiveArray; use crate::arrays::StructArray; use crate::arrays::runend::RunEnd; + use crate::assert_arrays_eq; use crate::builders::MapBuilder; use crate::dtype::DType; use crate::dtype::DecimalDType; @@ -899,4 +906,32 @@ mod tests { assert_eq!(acc.finish()?, Scalar::bool(true, Nullability::NonNullable)); Ok(()) } + + #[test] + fn grouped_is_constant() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let elements = PrimitiveArray::from_option_iter([Some(5i32), Some(5), Some(6), None, None]) + .into_array(); + let dtype = elements.dtype().clone(); + + // The groups are `[5, 5]`, `[5, 6]`, `[]`, `[null, null]`, and a null group. + let groups = ListViewArray::try_new( + elements, + buffer![0i32, 1, 0, 3, 0].into_array(), + buffer![2i32, 2, 0, 2, 0].into_array(), + Validity::from_iter([true, true, true, true, false]), + &mut ctx, + )? + .into_array(); + + let mut acc = GroupedAccumulator::try_new(IsConstant, EmptyOptions, dtype)?; + acc.accumulate_list(&groups, &mut ctx)?; + + assert_arrays_eq!( + acc.finish()?, + BoolArray::from_iter([true, false, false, true, false]), + &mut ctx + ); + Ok(()) + } } diff --git a/vortex-array/src/aggregate_fn/fns/is_sorted/mod.rs b/vortex-array/src/aggregate_fn/fns/is_sorted/mod.rs index cfff8ebbd9b..f1f951c08ba 100644 --- a/vortex-array/src/aggregate_fn/fns/is_sorted/mod.rs +++ b/vortex-array/src/aggregate_fn/fns/is_sorted/mod.rs @@ -569,7 +569,10 @@ impl AggregateFnVTable for IsSorted { _args: AggregateArgs<'_, Self::Options>, partials: ArrayRef, ) -> VortexResult { - partials.get_item(NAMES.get(0).vortex_expect("out of bounds").clone()) + // A null partial is the empty state of an empty or null group, which is vacuously sorted. + partials + .get_item(NAMES.get(0).vortex_expect("out of bounds").clone())? + .fill_null(true) } fn finalize_scalar( @@ -625,6 +628,8 @@ mod tests { use crate::aggregate_fn::AggregateDTypes; use crate::aggregate_fn::AggregateFnVTable; use crate::aggregate_fn::DynAccumulator; + use crate::aggregate_fn::DynGroupedAccumulator; + use crate::aggregate_fn::GroupedAccumulator; use crate::aggregate_fn::fns::is_sorted::IsSorted; use crate::aggregate_fn::fns::is_sorted::IsSortedOptions; use crate::aggregate_fn::fns::is_sorted::IsSortedPartial; @@ -635,9 +640,11 @@ mod tests { use crate::arrays::ConstantArray; use crate::arrays::DecimalArray; use crate::arrays::DictArray; + use crate::arrays::ListViewArray; use crate::arrays::PrimitiveArray; use crate::arrays::VarBinViewArray; use crate::arrays::runend::RunEnd; + use crate::assert_arrays_eq; use crate::dtype::DType; use crate::dtype::DecimalDType; use crate::dtype::Nullability; @@ -943,4 +950,31 @@ mod tests { Ok(()) } + + #[rstest] + #[case::non_strict(false, [true, false, true, true, true])] + #[case::strict( true, [true, false, true, false, true])] + fn grouped_is_sorted(#[case] strict: bool, #[case] expected: [bool; 5]) -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + let elements = + PrimitiveArray::from_option_iter([Some(1i32), Some(2), Some(3), Some(1), None, None]) + .into_array(); + let dtype = elements.dtype().clone(); + + // The groups are `[1, 2, 3]`, `[3, 1]`, `[]`, `[null, null]`, and a null group. + let groups = ListViewArray::try_new( + elements, + buffer![0i32, 2, 0, 4, 0].into_array(), + buffer![3i32, 2, 0, 2, 0].into_array(), + Validity::from_iter([true, true, true, true, false]), + &mut ctx, + )? + .into_array(); + + let mut acc = GroupedAccumulator::try_new(IsSorted, IsSortedOptions { strict }, dtype)?; + acc.accumulate_list(&groups, &mut ctx)?; + + assert_arrays_eq!(acc.finish()?, BoolArray::from_iter(expected), &mut ctx); + Ok(()) + } } From a720e52529b87615a455088952b8200bd0ecf4c2 Mon Sep 17 00:00:00 2001 From: Matthew Katz <87445739+mhk197@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:49:59 -0500 Subject: [PATCH 35/41] Push down `array_sum` in DataFusion (#10854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The DataFusion expression converter now pushes `array_sum` (`list_sum`) down to Vortex's `list_sum`. ## Changes - `array_sum(xs)` converts to `list_sum(xs)` with `include_nans`. DataFusion plans it as `array_sum(CAST(xs AS List))`, and #11161 pushes that cast down, so Vortex adds the same `Float64` elements in the same order. - Anything whose cast or sum Vortex cannot do (nested, fixed-size, decimal or string lists) stays in DataFusion. | Local A/B, pushdown vs DataFusion (time ratio) | geomean | |---|---| | 42 synthetic shapes (7 element types × list length 4/64/4096 × 0%/50% nulls) | 0.80 | --------- Signed-off-by: Matt Katz --- vortex-datafusion/src/convert/exprs/mod.rs | 25 ++++++++ vortex-datafusion/src/convert/exprs/tests.rs | 67 ++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/vortex-datafusion/src/convert/exprs/mod.rs b/vortex-datafusion/src/convert/exprs/mod.rs index 151228d48b4..89f6cf15356 100644 --- a/vortex-datafusion/src/convert/exprs/mod.rs +++ b/vortex-datafusion/src/convert/exprs/mod.rs @@ -22,6 +22,7 @@ use datafusion_common::format::DEFAULT_FORMAT_OPTIONS; use datafusion_expr::Operator as DFOperator; use datafusion_functions::core::getfield::GetFieldFunc; use datafusion_functions::string::octet_length::OctetLengthFunc; +use datafusion_functions_nested::array_sum::ArraySum; use datafusion_functions_nested::length::ArrayLength; use datafusion_physical_expr::DynamicFilterTracking; use datafusion_physical_expr::PhysicalExpr; @@ -31,6 +32,7 @@ use datafusion_physical_expr::utils::collect_columns; use datafusion_physical_plan::expressions as df_expr; use itertools::Itertools; use vortex::VortexSessionDefault; +use vortex::aggregate_fn::NumericalAggregateOpts; use vortex::array::VortexSessionExecute; use vortex::dtype::DType; use vortex::dtype::Nullability; @@ -42,6 +44,7 @@ use vortex::expr::in_list_of; use vortex::expr::is_not_null; use vortex::expr::is_null; use vortex::expr::list_length; +use vortex::expr::list_sum_opts; use vortex::expr::lit; use vortex::expr::nested_case_when; use vortex::expr::not; @@ -597,6 +600,10 @@ impl DefaultExpressionConverter { return Ok(result); } + if ScalarFunctionExpr::try_downcast_func::(scalar_fn).is_some() { + return self.convert_array_sum(scalar_fn, schema, input_dtype); + } + let (input, length): (_, fn(Expression) -> Expression) = if ScalarFunctionExpr::try_downcast_func::(scalar_fn).is_some() { let [input] = scalar_fn.args() else { @@ -634,6 +641,24 @@ impl DefaultExpressionConverter { Ok(cast(length(input), return_dtype)) } + /// Converts `array_sum` to `list_sum`. + /// + /// DataFusion casts the list to `List`, then adds each list's valid elements in + /// order: an empty or all-null list sums to null and a NaN poisons the sum. `list_sum` with + /// `include_nans` adds the cast elements the same way. + fn convert_array_sum( + &self, + scalar_fn: &ScalarFunctionExpr, + schema: &Schema, + input_dtype: &DType, + ) -> Conversion { + let [input] = scalar_fn.args() else { + return Err(exec_datafusion_err!("array_sum requires exactly one argument").into()); + }; + let list = self.convert_expr(input, schema, input_dtype)?; + Ok(list_sum_opts(list, NumericalAggregateOpts::include_nans())) + } + fn convert_case_expr( &self, case_expr: &df_expr::CaseExpr, diff --git a/vortex-datafusion/src/convert/exprs/tests.rs b/vortex-datafusion/src/convert/exprs/tests.rs index 27f2632810c..6f71f8b08dd 100644 --- a/vortex-datafusion/src/convert/exprs/tests.rs +++ b/vortex-datafusion/src/convert/exprs/tests.rs @@ -18,6 +18,7 @@ use arrow_array::StructArray; use arrow_array::UInt8Array; use arrow_array::UInt32Array; use arrow_array::UInt64Array; +use arrow_array::types::Float64Type; use arrow_array::types::Int32Type; use arrow_array::types::Int64Type; use arrow_array::types::UInt32Type; @@ -114,6 +115,27 @@ fn array_length_expr(args: Vec>, schema: &Schema) -> Arc, schema: &Schema) -> DFResult> { + let function = ScalarUDF::from(ArraySum::new()); + let list_type = list.data_type(schema)?; + let coerced = function + .coerce_types(std::slice::from_ref(&list_type))? + .remove(0); + let list = if coerced == list_type { + list + } else { + Arc::new(df_expr::CastExpr::new(list, coerced, None)) + }; + Ok(Arc::new(ScalarFunctionExpr::try_new( + Arc::new(function), + vec![list], + schema, + Arc::new(ConfigOptions::new()), + )?)) +} + fn get_field_expr( source: Arc, path: &[&str], @@ -2210,6 +2232,51 @@ fn test_native_nested_list_length( } } +#[rstest] +fn test_native_array_sum( + #[values(DataType::Int64, DataType::Float64)] element: DataType, +) -> anyhow::Result<()> { + let lists = ListArray::from_iter_primitive::([ + Some(vec![Some(1), None, Some(2)]), + Some(vec![]), + Some(vec![None, None]), + None, + Some(vec![Some(0), Some(3)]), + Some(vec![Some(i64::MAX), Some(i64::MAX)]), + Some(vec![Some(1 << 53), Some(1), Some(1)]), + ]); + let data_type = DataType::new_list(element, true); + let batch = RecordBatch::try_from_iter([("xs", cast(&lists, &data_type)?)])?; + let expr = array_sum_expr(Arc::new(df_expr::Column::new("xs", 0)), &batch.schema())?; + assert_native_matches(expr, batch) +} + +#[test] +fn test_native_array_sum_nan() -> anyhow::Result<()> { + let lists = ListArray::from_iter_primitive::([Some(vec![ + Some(1.0), + Some(f64::NAN), + ])]); + let batch = RecordBatch::try_from_iter([("xs", Arc::new(lists) as arrow_array::ArrayRef)])?; + let schema = batch.schema(); + let converted = convert( + array_sum_expr(Arc::new(df_expr::Column::new("xs", 0)), &schema)?, + &schema, + )?; + let session = VortexSession::default(); + let sum = session + .arrow() + .from_arrow_record_batch(batch, &schema)? + .apply(&converted)? + .execute_scalar(0, &mut session.create_execution_ctx())?; + assert!( + sum.as_primitive() + .typed_value::() + .is_some_and(f64::is_nan) + ); + Ok(()) +} + #[test] fn test_native_case_null_conditions() -> anyhow::Result<()> { let batch = arrow_array::record_batch!(("a", Int32, vec![Some(1), None, Some(3)]))?; From 068d8194c8d5b301501eb531ab4bd03403312958 Mon Sep 17 00:00:00 2001 From: Will Manning Date: Wed, 4 Nov 2026 14:00:19 -0500 Subject: [PATCH 36/41] Docs: draw the docs with the Vortex design system (#11207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This draft PR changes the look of the docs to the Vortex design system that vortex.dev, bench, and the Explorer use. It is a prototype for review. It does not change the content or the URLs of any page. ## Changes - `docs/_theme/vortex/system/` holds copies of the system's CSS (tokens, docs, and focus). `vortex_theme.py` stops the build if a copy has a hand edit. - `layout.html` writes the system's header, navigation, "On this page" list, and footer. The pages need no JavaScript, except for search. - Code blocks use the system's syntax colours, with a label for each language. - The Pygments accessibility styles, `accessible-pygments`, and the Breathe hook are removed. - At 390 px, no page is wider than the screen. Today the API page and the index are wider. The build passes with warnings as errors. After gzip, each page gets 0.9 to 1.9 KB larger, and the CSS goes from 2.7 KB to 5.4 KB. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Will Manning --- LICENSES/OFL-1.1.txt | 85 ++++++ REUSE.toml | 15 + docs/_ext/vortex_theme.py | 49 ++- docs/_static/fonts/geist-mono-latin.woff2 | Bin 0 -> 23108 bytes docs/_theme/vortex/layout.html | 94 ++++-- docs/_theme/vortex/system/vortex-tokens.css | 110 +++++++ docs/_theme/vortex/system/vortex.css | 249 +++++++++++++++ docs/_theme/vortex/vortex.css | 322 +++----------------- docs/conf.py | 25 -- docs/pyproject.toml | 1 - docs/specs/versioning/compatibility.md | 1 + uv.lock | 14 - 12 files changed, 595 insertions(+), 370 deletions(-) create mode 100644 LICENSES/OFL-1.1.txt create mode 100644 docs/_static/fonts/geist-mono-latin.woff2 create mode 100644 docs/_theme/vortex/system/vortex-tokens.css create mode 100644 docs/_theme/vortex/system/vortex.css diff --git a/LICENSES/OFL-1.1.txt b/LICENSES/OFL-1.1.txt new file mode 100644 index 00000000000..84b8ae48080 --- /dev/null +++ b/LICENSES/OFL-1.1.txt @@ -0,0 +1,85 @@ +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/REUSE.toml b/REUSE.toml index 2d0a8185788..b371aaf2d67 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -6,6 +6,21 @@ path = "docs/**" SPDX-FileCopyrightText = "Copyright the Vortex contributors" SPDX-License-Identifier = "CC-BY-4.0" +# The docs theme's copies of the Vortex design system's CSS are code, under Apache-2.0. +[[annotations]] +path = "docs/_theme/vortex/system/**" +precedence = "override" +SPDX-FileCopyrightText = "Copyright the Vortex contributors" +SPDX-License-Identifier = "Apache-2.0" + +# The docs theme's Geist Mono is Google Fonts' Latin subset of the font, under the SIL Open Font +# License. +[[annotations]] +path = "docs/_static/fonts/geist-mono-latin.woff2" +precedence = "override" +SPDX-FileCopyrightText = "2024 The Geist Project Authors (https://github.com/vercel/geist-font.git)" +SPDX-License-Identifier = "OFL-1.1" + # BtrBlocks diagram is licensed under CC-BY-4.0. [[annotations]] path = "vortex-btrblocks/decision-tree.*" diff --git a/docs/_ext/vortex_theme.py b/docs/_ext/vortex_theme.py index 59069cb3b13..0694c4977ba 100644 --- a/docs/_ext/vortex_theme.py +++ b/docs/_ext/vortex_theme.py @@ -1,6 +1,7 @@ """ Build hooks for our theme. -1. Add pygment styles to vortex.css +1. Join the Vortex design system's tokens and docs rules with the theme's own CSS into one vortex.css, + and fail the build if a copy of the system was edited by hand 2. Load only one CSS and no JS per page except search page 3. Rewrite TOC into detail/summary with regex so it loads without JS 4. Minify CSS @@ -8,23 +9,26 @@ from __future__ import annotations +import hashlib import re from pathlib import Path from typing import TYPE_CHECKING import rcssmin -from pygments.formatters.html import HtmlFormatter +from sphinx.errors import ExtensionError if TYPE_CHECKING: from typing import Any from sphinx.application import Sphinx -_LIGHT_STYLE = "a11y-high-contrast-light" -_DARK_STYLE = "a11y-high-contrast-dark" - _DOCS_DIR = Path(__file__).parent.parent -_CSS_SOURCE = _DOCS_DIR / "_theme" / "vortex" / "vortex.css" +_THEME_DIR = _DOCS_DIR / "_theme" / "vortex" +# Copies of the design system, written by its tools/site-styles.mjs. Each starts with a header +# whose sha256 covers the rest of the file. +_SYSTEM_CSS = (_THEME_DIR / "system" / "vortex-tokens.css", _THEME_DIR / "system" / "vortex.css") +_CSS_SOURCE = _THEME_DIR / "vortex.css" +_SYSTEM_HEADER = re.compile(r"\A/\*.*?sha256 ([0-9a-f]{64}) \*/\n", re.S) _GEN_STATIC = _DOCS_DIR / "_build" / "_gen_static" _PAGE_CSS = {"_static/vortex.css"} @@ -36,12 +40,19 @@ } +def _system_css(path: Path) -> str: + text = path.read_text() + header = _SYSTEM_HEADER.match(text) + body = text[header.end() :] if header else "" + if not header or hashlib.sha256(body.encode()).hexdigest() != header.group(1): + raise ExtensionError(f"{path} does not match its sha256 header: edit the design system and copy it again") + return body + + def _css(app: Sphinx) -> None: if app.builder.format != "html": return - light = HtmlFormatter(style=_LIGHT_STYLE).get_style_defs(".highlight") - dark = HtmlFormatter(style=_DARK_STYLE).get_style_defs(".highlight") - css = rcssmin.cssmin(f"{_CSS_SOURCE.read_text()}\n{light}\n@media (prefers-color-scheme: dark) {{\n{dark}\n}}\n") + css = rcssmin.cssmin("\n".join([*map(_system_css, _SYSTEM_CSS), _CSS_SOURCE.read_text()])) out = _GEN_STATIC / "vortex.css" if not out.exists() or out.read_text() != css: out.write_text(css) @@ -57,22 +68,34 @@ def _filter_assets(app: Sphinx, pagename: str, templatename: str, context: dict[ ] -_SUMMARY = '' -_TOC_BRANCH = re.compile(r"\s*]*)>") +_TOC_BRANCH = re.compile(r">((?:(?!).)*)\s*]*)>", re.S) _TOC_BRANCH_END = re.compile(r"\s*") -_TOC_CURRENT_BRANCH = re.compile(rf"
({re.escape(_SUMMARY)}]*\bcurrent\b)") +_TOC_CURRENT_BRANCH = re.compile(r"
(]*>]*\bcurrent\b)") _TOC_CURRENT_PARENT = re.compile(r'().)*)
', re.S) _TOC_CURRENT_LINK = re.compile(r']+>") +# The page's own contents: one entry for the page title, holding the sections. The rail shows the +# sections alone, since the title is already the page's h1. +_PAGE_TOC = re.compile(r'\s*\s*', re.S) + + +def _branch(m: re.Match[str]) -> str: + # Each toggle is named after its section, so a screen reader can tell them apart. + label = _TAGS.sub("", m.group(1)).strip() + return f'>{m.group(1)}
' def _toc(app: Sphinx, pagename: str, templatename: str, context: dict[str, Any], doctree: object) -> None: + page_toc = _PAGE_TOC.fullmatch(context.get("toc") or "") + if page_toc: + context["toc"] = page_toc.group(1) toctree = context.get("toctree") if toctree is None: context["vortex_globaltoc"] = "" return html = toctree(maxdepth=3, collapse=False, includehidden=True, titles_only=True) or "" html = _TOC_CURRENT_LINK.sub(r'
{_SUMMARY}", html) + html = _TOC_BRANCH.sub(_branch, html) html = _TOC_CURRENT_BRANCH.sub(r"
\1", html) html = _TOC_CURRENT_PARENT.sub(r"\1
", html) html = _TOC_BRANCH_END.sub("
", html) diff --git a/docs/_static/fonts/geist-mono-latin.woff2 b/docs/_static/fonts/geist-mono-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..cf24126b086f12217076c4902c7c6d58e4f52c75 GIT binary patch literal 23108 zcmV(_K-9l?Pew8T0RR9109r%<6951J0H*u^09n!i0RR9100000000000000000000 z0000Qf-M`66dZ&qKS)+VQiMJ~O;$ltJO*GtQ&d4zfj9t#5HEfa2nzCK+XV}R5&$rP zl~MsV0we>75Ck9vi3SIVLmRx8HEf$!1G^m{@8|1nH#<z8Z*cTgpNpFGOE!f`CPn%(z6QuP`KInd zdbrye;7*Q+R6Ge4ebf5L>d^@B;}LB7r&6_QBm!)N)Mzv!8z7iv zP@*V~EN87(8^iByvbphI_xN1>_r5uMa&VF}C1)yFBIjS7)d$C6LSj<3H{?`ZQQhme zX>RcM%{6_<8>07~H#rYxKn7WWgkt(qMhFpfs54KJEg2)5lRS`<2fz+n$MH`26mZ!A zacY+Z(SP2quX_uFd+LoCJ7Wf=&_k(N{eS&2cDE~ioerREVFkdU&VOYH`2%v>jbKOE zw%94qGIyDP-~lFViY!YfSvauW*gF#T-`bE_pWXW>hKN*NCp1D~dH*t5ll$A1@sQ_LsfcxbCRLxp^eSyJAg1PUBa+Dmob}l+ocIArCMdhX(`~f%|j79({ z$rcFF#n?_zM?6w={;J8}@(8DJ2mh9bsRWnm7pL?lJ6!SoOfox~8D*lY9HKJ`bK98hE( z+j7o=fFa-Zd%v7SLZ4GLmm*S($o%iu{y+As?_%ogr}-RDxvb+D5g{5P;Z4agt9kDa zD&UrMm<&JS@ZaY6I0$D#n<|J1K|@5#`VL!-aAdvawsYN6BoRaOAnap~)}~slLv&h4 z8InJ|ECjz=2r6C27~`;}nInM~)Tzu$B1F!y^gAhHYRxbk!Gi>Dfb+iN0Reyk`a#$9 z$-=#SR(NvtMe8LnEe;*EkMzDjK#Vejqz&%vFvvRMu`dC#@BpgS0AU8`fc&c|x)aC(tmUBagT0V(kq8Vv z83!9Bx^guIFU-y*IKb*x`!TyNepYpA~4R7(QSt zmJja|3a>lv{r@?jD6(@<_%jm`H zKOfz}-mU{ul{(bQT^P`}Hc;&#wu3er*=}mw)C) zz}Y~F0vzyr2qw(lf{4az&B|G&vfl(ad$2Ux8P{C^^ZUh3R6w2BJ0pO8Edb3*>`@Zo zjvE28(1bB;$ANGVO5|<^o9WeP=$^_MG|y&Or~0z68rGvhH@4vpC*}Y==yk9LA{%!7 z0(*)!Vzcx1!jqj;+utBYsSm^B4OKB%lyEWyY5Ya+>(31#=ht@HbP9NWZ7*z0Z2-IM z{~@pXkd%%%(}Z>DQVDe~tCB)oqHwT~!vFF$?i@{64kYJ|yq;$PMn_MNWErs0?t&PD{0k4soeoCcX_z$mt2pVbf1^pPYnupyDH z;~et|O#il9K&|^=Bp_Y#z{y(HhKJYeh9+Z2n`7;GY}{`c+;~17p`YMb3o+L}8^YiJ zIo=H{hl#E@(EZ<6ZUCg0T}_=puwKo+?vM8Xn$|g>1G~ZzCNj)XK71w+S4k8zyyg?D z%#|Q}7z$k{-OXB^FcbdgVM38L-OtbclR6gYm!u+t)uZIPI08 z;psDG&YBH?5R9N0PLLGMupBRllB}qjZkU$sxSk(`QJkb%UX)ecv|T@p)4Z(Pew^3+ zyx+gMwY{^ucfYz{dr)s+2hA3KNVGd$lIqb6%khFJ$%?A!hH2TzaXmi>qqv_~#5bFb z#*-<`s|qBUoIHi(D~!OBRKhxZtacLnul{4W>9^gdoVNBAIIMgPby}kpu=f}EF+AJF zRUpAGEA|4|JSx#%2%?TnM0p)M2RPW_hxwuGw57L$V+d16_-E-F0Ny$-Jpth6uWtdC zZ5%+eyO3fE2bOS2(#h{_eF(*$K9EOHKc4^ro0J8tL8ocvM{7|HF@PSa<*celS!d*Q zP17|1d!wkG_kJ)^EZEz`GSV`HR@=e@~a4QWbq+NA4ri(Vx>(L&1pyydl4>@tUDJaTJn=gxY@fpac$ zqDu*lFM2@FPH*Y;yHJ(A1z6gApcx;zwnX?@_afoaY1+M_ zLZbhe#T#`JdE!$L(BCW9v%PEk?K5-BRGRWe0+wa2RFMf;)xZLPLu@%Kh^|U40iAut) z$a7+$frJRRan~_A%>1YJAo=D%Lf=WXdYn%|IYN2x!D$yeDX1!R(-o4s5CW5LoFujT znE(cY!ns9o3`b#(dDIByD`9jMAbIQFwy;9yk8Jv2+O&wDF+$GH5bM~+jKr}MHc3p# zwX%Kxyb`sE%>6#N?N>4{@e2Ap2tsV}qy)#hyohb^PBM zNc|4TcLy+>?BhTi;R2jc?79lEqJ$Do*Ea>BH`vpL;amK>5hD=0 zR~La13uCuLpnXg$w@ZMbmJUI4jfrlg@_l0$jVPq59pV@*>7=s!9qykKm+b@dqcXTE z*OXEy%F>YjV_FiVg_p+A;rg0iCP5hcPH#Sc{Kj>RsFx$N()-MzXpSJOt7K#^$=)^xrX6cSDBSp>!*-+=zvzlhm8*Yu%BX=C+1(nR40?R=<6bBP)<* zlBX(!w++At6IL{#$W-Bm+BJy$Vy_N;Z6G8VbXmWz`ffS$?L}|5VEs)1jtx3(0(oVB zjo3;4118_?DRcJpQ6WxvCkizx^e;4c^yO%!B}TC5R%|*7+@A@bs#{&HN}CqO=KDe_FnK^)iY2esKrn!(E0wT-V{eW@#qt2G zoE2?fbkujoSHQ@xfw&t1XpG>e@_C7VR1$K4`FZ#9z0U^{9+XamdblAh4nQJ?-5|!= zNOngI7@^?vro3*YVm?(I_Tkf>_faY#2U z&lT)m9M;Z)7lc)#dh&4o+f0fY`Tl`x)Jx>#CKp=`pXKZ}a>}qr!hdE$t8s|gA!QOj zn~e3^+Y2-A|E4)zvjN|Tqg?faf2VPuGADfn@AK;*Zbnb!2N*h!5qs~l3^N>lXqS;# z^liDSSm|e|sWx+Ib<(||kU`_D=p^h$!gK$*({N9|v0M)q40*kix_tl*AnffKG_ z+0UoG3QfXG@lO6!C`!_Kx8`R~;nMCYpTD~`f5cJ?PDco?GL>HbGi%^}oL=}-y&oid zv5ogu(Yj0U^baYRzq3B}UoUBWTH=oLUAOhFi~Qu6b;G&Ex!c&AnxRqR5sn|zXW(Ac zwOzN(K@Tj^kO@Usx`(<|HEU$p3PI6m*Kv8zyh-2q?hWiSAMNsCOLX*OAAG9)=9qQk zv$cyCogX%C=jhuy^PaIb1NU2--`&Q-zQQ6Bvfyh&$d#BSd3!})d#U57W`~2LL;6kE z-Q(;xl4U>o!eE~nP>f}*#$K)LNNZ)7w@RX9V{Xh0tGhQ?!1rV*<<9iQ#V|C{)k@ir{Icf30vR30 zZ9@7ucX_n`yU`9&Lo#{bG--UO#-xMl&a;z^o4HZtEDY%cZd8e72koF+=OEn(k=B3v zN%uambm1~L`XHY<^6kK4>HU45Rcmt%Ylmv;fzcVyp1!z1G=%;S!d zzzGAbXcoXxB6^raA2OYRM`vuN#(QM|b(;o?y}Ee6gSa~lH@vz3GnyP&?@3NnT34Y* zr`T#KR*2EQKS1gnnG{Hq5J4VgmL;rztqzr!_f6Z3RSvp08I$Thnc53C9h7c3bSF*1 z2&HL7XM}BD#a_3kXZM*rLzjg2vA*Yo#|g7;6)WAfyP?Mqe=Glvv0*KVaaW_Ha4>9y z8z~Mm=`rMmOvgb(!4qpa=#eYm`S9rH z#P^}vv|+P%n&)3qyGYd@`+ClqWzC)*9m|i$u))0p=2FLs4$7<3S;5wIGj`m?V^on{ zVSRGUI|KFYDP6e@>(2mF{0Dgu6UVQ#F1ywirWS`)SDt-)HLJYwu{~^1{N3rkHID2X zoU)vAW!@|t8~}6szuoZGU)Ffm{I(ay^kYx_YM zsv@)6r!Ms_cUTl+nXMXH4i7+jnMfR1XaH1Z`}#x5jXk~O+#mMU%^R8&uR-S6Jr1eN zuA|S6)X-<;n@y(n`Lf_}3bt>-d=MW0B(m58ec00jeXw|PWU|-M*0XTYqMpTV4)37e z35S1idYr#SVGtWX=vg|g$n9|p05jXx=h1H%>Q!&-4|}ySnZ6}U7eeM}o!)ARL&umM zsbS7?>SRJo6|=5%)bq5}o&53Ompqr94V2^l(5~gXLViH)uC>4o@51kb4@P!?%{H|v zsHp?L zlex>xj5VD5hz3%Bv;))l5N7d@%l#Ynrxk#e4fpw!8waMD$`zPiKW)I`^-Am-`mEYo z`fR&aLZKGD2xqmN!B^RhZDV`)wjFKcnnc{Df<+!r!9s2n8pq4xX(n9>>rCTmSrvRO zdv&!3;A=k6kH3UJ`Q?4j`@iipo^ch53l$9p1FM`1nu}|j2D=OztxFG213j<9$0xh> zdaYYC`S^X@-6c@eowP-}gzjsvkNd~L@W33mf4z7cTh#tgnD<$h{B~3LSIIPfwR=u? zlc3UWW_c8*WRn7^&%NN!IYwBl z0-1CRuJGhrabW?&B2r4j?Ea4 z2r&r5`(&<_TgWC5oQxf9h`m}m1)@hAPd3HgBm_Vb>Dv0}T3g5Hck4+&;9piU1YiIB zC43zQzoZLXLe{Z4Ys*7#6R8ZaF5MGYxnN;n#8YLK(fUI4C&`o9L26m*M=%N-Fj-;BM?}4- z+UD1$`qJG5Bbl@}=tX9DA+H9qCc3EmQjKO;rIkx{+j9( zUNIep6~l1@q^PM-Odbs&_nM?atEVjzL1Yy+!%rI@cU0xC^J(z7 zJ!SCn9*VJq;^M`-h!Rw)DFGvgueG;DYV7UcMdmF2lvxcuvy~fWc{MG^R(GV%;SO0TuJQTopHaY1w{_~}(_vUX z++na9+ck>eP|@5J*zGe`>3C;CD4eM;{I7wAv^&fSl@kJASD7u4Z4??UM-i)TpMg$5 zfU#CqLsc2K1(18st1Nbz+|Ts#tMqMJd9Teb>uJ&Iq(NZ`?EY6R3AS>eOvB_lFd0R0 z8Y(y@d!5JQtOK!en7R7BN5*2N%qd`m6+T$y5L=}hy8}`JH}uWNi|<3_XAeLJU=V); z`h?!d`)lzeNQ1SS?QRerhrd9Sc3-)5o!7GMQEPlD$SEr(2njQwIaUa=;uvMnEeJ5M zj->w8OXUU*SD;5~F)P$&o<RJldW zu+#^uAemjs)RJgAz6!9?t(^uIK9x(>QbBd7T5<6h$8C{_4O_`C5ZIFL3BEaVcJOtN z$fBh5`)Mt7Tw@QuG3(gHF0|_C!+tk?hSel+6{)d7m1F^RQDJqAR4U}EK6-zx2QjQ< z65R@1Z+zlJ4DM4!s#UI-gL@^kLS>taX(~a?8B2ZRn$g)>TZ@iHj*dkMXtm^ko;9*m z#7|!H`Gh`TIT{y=?GBc{@Si)ol2=}n=pWVcCDt>ISSa!tq>_(b zeKa{E#89xgVqF~>xF708jSW@WXuR2D@cB(CU@OcQc*S+|rcBegH7;1Cspzod2oHR!CVbGtQ{(-En6xg&LU z4f9-;@I15PQ-Sa>7;G+h;Yy}p(^N-yNU^b}SL+TtoL%8})Y}8DE9CGBOh(%5K!_A- z0=~KQLIH*UQGn8{!%ze57Z<}cE{1QxKw#rc8;G|{(mvfqp+3CBwfP+wa|oH~=+_Z!)o;-xU4X7>&!QUXCFQUgmgEL-94Jns9JQjQdsQoqqKBu1%D zZxAIy$TZhrwAI+m5&IN6kK3Jv^`|wrIt|+TUOt%rZp7*PK62F$``-=ylhOc~*~S*W zG6aQ?!%n5j=z(nlvPDPZR@fQH2xnvnW|bo+tmNl%LyKA(H=9+7P8F!kc6VFkE1H9) z?`Zlubgt?EQ*8Spgg&%Zq44TB3lOoq^_^~t(A27sby}V3?u63pHTi-r$nDefY&4dc z&!^eU72sGxd41|7XLv4r%c-+9yrFu#Y+V?zvQ1OD+FGNdCT7dcI|kWxQc0jeP+i?9 z5C^0>tNluzJvSV47<=s9T$9%n(&*JL$nA1NYCSJwjK~a9nNX)t$n^>#)dBOH^1kTh zEw9BcxdPTz_H-#tuNiP#-TtqJy;SS0399fhgHo#eXva6cU-0y2)Yab+rIM;CcSG?q30+!t5n^IQ?cbLuAo4F|_1-sI0GH@&` z`v1si*6@SF9+c2=sS@CWA(Di9il+)XT!-W->_$mDe!y6LXGkt0zopvzkiyl7 zRO$y@%EQh5o5&)oW6EgD%gPqgsBb3YeafM-KTGm|FK)5l^Gp5&{|-$b0(aJm){2Lw zbt|@joGT=BeM%GKSO2e!lqOU?DRhP5?Q%R45-MM!>&x#=zh%LTvlcd7-P(5*U!~Wp zAiE814k3|Rb#sTzs*MW8HOh)@OcIe1Z*OA>2gDxn zfRNQ@->)bTbOtyMPQ)hhThS;hN)oU|!bt%qI6e5$5v2hO3S~h{{bv!}&-~y*Tocil z{=4!YSQg)eroC#7+%Vk8@?TB_@#}F?VB36VNm>`LgTm>!i^MgGs77%Pm zj@2a!9{y19Fi|J}69MRk%%_LNIx?TK#n=nw(V-rT=KUFcoNeB|Zkf)L@Je58e1$sy z9)om!CT4c#rXqnP;?mK(b;gQ95e5A&CSpPv)2E;u#nh@S?3S$Nz+`H_yIRZlaW#$P z*J3gDOs`T_b~kC(>YU5X83vkQO>sVNlCx601Ki1O>5tZMicCtaA$}iO?uhs&yM1o6 zOdobtt-V~BYZH3W>6ES$a7(lOLX4nt3H1et9pA&1My^1okWLL;RTNSBDU3rj+?z{4 zmO$*|@xyAhFdXEGZ;!i&OhZ6aZHRJYZk-=F%P7@y_>KAHno4K3KFdoF(AX1S?N`ZM zp&>$5DZ_M$t=Wg1Vdl%q5l3<5I%}RIFUSg4GTvPE+AxPD(nYA{5^e{XSO{ThC13B( z&p&S}0TtKt4&#X?=)>KQK53qO3q7@LtjLKx9vKM6@ z-^jEQ1*WlLfu>F&YEmj?hdQ}N(;%m{1_j$#(e%Te%PKGV%|2Xh=;QRS7nVJqgnLjX z1PfdGPp`C1L+H@R2kN4Ain`@A=`Cp~tzJ<-CN{u&hh=6~m?|qP%w`5ldKeYvlBsnw zSjkybS~-bSPNU+%pSLT2@`--}$?=Fg0#cSXIh!1h|1@wrU47f6KX@4-C~i;;!?0p{ zTt6Z)SsUf`Qfa+hZYrN%PV+J&?Lilv-t~Y+e^3v;{=t?r1{N$BET6w%{vi1a2!r0h zN^ilyrvy-;A!w+`I!?8aNI$?d?<8wpSj4dMs3*|Qy`MxGt<`Hu(=$kW#B2xnvb6b7 zFQzko2dYmd@u{ynB-z;0t7pt1Bm*-#90o zvGfPvnn1EFxHOSyd6dQR0xf_g#A8cJu=w!&W$nLZ$saf`I!9p2=z+_k12e(DWJiWJ z`_G*30NI4QLVpeq%+Pr#z5TrNeEOJ^zZ$UcFY@SD(iUWT@jj8uV{VqyF0Ed@F0Xh~ zRmKALXm^I84NpP5Z?U^W&>zpsH~@&e@d>Q7i)<2&WwOb)bqiKPA})39#dF}DxY8T; z`Mn{f(jvdFoIXaJA{I{}t}^qM&8O|Ri>pDi7(lMf&dSW}FeWQY^m>o82gsN1!mKDe zGBc^N7AN>577a%ITm=&p3q-!qtUB#1aKoon5z0RDMdsJ7XmT{=++e^8t87N5T)Trr z-ockMAtPW_(+za@yLdz+b`X1zgVx5poQzCGHg+hlswm7TBxXL}EhA4!sbZ+X#%@c7 z1sEMl;XZ8k-2fYY|JU5t=6d-Te_bX;VB*GnmoTXO=DmMU+k!8OivOx6@9zf3c3-DV zw>nGzzW}&KU8kGCv251@gEHS8mm6D(I*AzK)$TD=7dyC767FDNh2 zrF{6wW+H4Ri!3LXlgr6O7JQZ+mQv#5i{83w?PFHGeebG=D=q-c%SheD6I&>++AOKGn@CA_nP;a z_nQxx55hz6mU=krp#nSMJ$NtPhxg+H_#i$6567L>bW?5MJ(3Lr8|D`77u&I}Ul|<2 zOm>$aMeBV!4|+U|;&CFo+W=r0?5WRKXBmfuvMn#r#-Y)(|cYzmdYkpFRdz8H(S)Y_haA zYjc3DHA`0$t?V~kY;N6d1X?e}?pts)KHNCrR(6(wxyqlO5AIgPT?XdW{^`(VwBP1X zCUz3f?qS#o%$2N?`G@x{6O>SJ0&^v^8gv_H^_#?2NT_)gHlj8R4rk?wrAfX@3@j~VBY9GLm)H>!`BJt zoN>YY!G8y?s_M^;0?ZFy)sKBw%h(KPbLX=O^0``m#PBN2?LOr^)g5{)KRv2! zBepr)<-Y9(cY4N3(-p3@zJ^=W?v8fT?z+M#$1ORGt1 zOY2LUowht}Kc*Pt#Dp>ZnCY0snAb6DFdt*~V6MSmRAf3QeO~&(^rPv|6R8B)uM1a>6XA3?7cPQ(4|f1JF)5wYKk0COZoVl0!^L-hYyOG+ z%lX&xe=W!IHPb`;X8#N7w#*(UN})?inbJu6@5;>pErt= zigSxAibciR;>O~E;$_7j7mt;6m;6(jSGuE&R<^F}B0d9Oj%VRzcpLs*{LAv5@{#=={d6Pe$ z(KcZtsDG`){jYZHyzsBz-T!rX0Xc`J;AV^xcCiSO!NY%?AQd11fbKbvLbgG?fQVTn z5J&HCEN9SOfC(ru=(*#X;$H!mFyCB&?}aaIneXw1xq0sm)y_ z=8d-&GN%8)j;91XOBa%he*7--1MZ2hF;vMWWzHQYt0UU7#`PeAL{1-h`Rf-=7vs-w zJ^y)==Y#O`tmFAglZK>4!)4u%zis~R8T5KfaUG@o_WFR9K&9Bpx_7l)8ShkJCLkq4O_^a(5hUU_1NIDh{X&DOSIoVRP@(?#1gsnn%B+>Ury>pB+*}N{R6Ye+Mif2WdV$cy=?m|LoFdx&xWz zOmzgT!BQETD&mvD-YqS?!PEM}|Gi!QIs%t)YS7DDPHpj&(F|9&8qR|4;^gPTszr)o ztj6SemqfOhkkAf1pTxO#nOGS@r~>zy`tm|C2=2%g@y4L858A2*vNVo)P1pUKN9t^H z1t#~m>e~31AFowa1doc$(R4znbRUpq;|;R?6XZP$;5WXvaPN3mJZ+D~&KVaD6oqmq z=QKj!aD~O?e1;Cu!n;OjxJ>4lp!*mO^uqWII``jeH#(h){``9N9%HcDFy5Wz@PSfm1GqDYzL4MWZ;7wI(q#5p{1TGH zn-}3=%D~^)_!ByU#^2)IZKLv5>6-)r4PNa)_C{ywr*QZ`*KQE)ziD#r zYgBL>9_W(gHD)uwGdrLIZHgsDuMbi1G?5a8^^(+0_&dS zetj6WuE7Jg4)J8&0HiU9D?}U)zioa&ey@Y@(l1*th0JgZ)~4RT^wZz|cHe-zt@Zr* zyc9yv-pAiHv#rEAAvW{0xGZ^tv?9T@)2m|k-@0G$dWzN8cjn5QH&#}@Vm^bA0E%|!P;g8| zl>D5lBYK?$*eyHjl%*r~-6#Qiv4fXJBXDkpiku`+BHQqfTVJr3vWwyP6t>iF`N&_(UxRf%eGYa;fz_k6f*axpJ_Oc*D+Ir;wuAD|!zA(B12IC#* z5m{NgQ^MTo z&D3f^KzfoMri8^bp)T62UvJlOB%Kknf{-|7tBP?}ny%Vz4#|^fY+Aa=iAre)bF(t2 z^ef&A1W3P};fem|$_VNKZ(1<2wEwyGCuO?wGsq7%8=)%ZQJ9#H+atwUf5oltD*2qD zNWqHgIFlx~#oD@y-VROVkT?o!PQ|VPqhkQD1Y{aeGKQx!BQOqtcNKU`*n~j@Z=+LC zsv-)@Cj!w`9Y;UPXDB=GlxU}3H^6HEn8~2TE5~&}_lzN&=3t^dZs%ekiGQawaUC~dDI+oL*BrczIHG_sqL%(tR zwj5p-@8?z`=HlI5lF#Lvd%(Sq|N5GuLmVrZir=W}&Yun9TvYd*jvx)STqJi4FC+N> zSvlX1=bsj@d|I)Cy<<)}ATV!1=MBJUa;DcI1QIKV55>C$sklKLCpPLj3snW z=MsUrni$^*8VZJ1At){oPtK+UTu3=1E~irr^I5og7F-(#U~=QbW~h>5ubDZ<|3t0=D19v{DR_k8ruP6)}y^13#V_ zvyGccWnr;tAzTV#USuprqLb-ShRNHmsB+M30yn}RTmDIdpL9!1=EhBXSy zS(qwHE}3l5!GAS7aAuenH#237wjr?s@kwCI^qBVm!zRUfFbSD~Qr5iU0M~@Ke-nH< z0R$1j3G52#gwXZuyAVs6790_Gwoub4@t~yVVq84CxTzf>fM!bfodIwo$Vhth z#ZLU`T(KPL{Qo}R_W*@aOH2098y??tPF&K~iCjG%tBtRVpmnf@^lR-wC#gPEL0K;4 ze%eIF%O#}I0Yk_HU;$fObwDy*W4OVfp%`vFW2A~COdsq>>zK~CJbFJN9-MB*Ogw5S zY0Ruml3SPwya9PK2=`bFR<9S0mDSgv9wop2P&3mZCgE1fhz0c0E6TqAWrMteTZ7zcmwwkt*n^YVV#h0 zTzE8$lgf6~69W(f3*+=8x8AfjqYDAvj5F6t!?zC1Y;F^~f1K8<=R=y5?_{>MHqOd@D_4qpsB&6j+@uCv#@GlbtFeIx%qh9X87OWB*QRKyi{p>9->EkbY39VyHjd;+QGPYd zo$l36dJQUbvjvk?;%gXRL(*h5#?D-91%%*-h&H*{E2ig~wHK6CxnT^gBB-)?zqAaA z;=D((nlJEM!Gz%o$X_`W9a@4oR6YayHy$+&nCQp1Fa)rEuak|z$$Kot{Ca!KP20MX zN|~XCrVVi0j^yx?{06tDt3S-qV9q`DM8zIZSiK^u#&*X|nDJU2{ipwa z<(BKo+(q?41KO;AIi1raymrb?(tlbgpH|4I{^#kzyJHNj1sGY%yPkLaTLtAfH|V(z z22@J=3yEjM%S_t|uH%IZwWW^jpV#D9f48NKds~c_k+%Y!)&7h6+_u4$$mam-Jgg2l z3#2TBqM!f};4xsJbDDh|UV90cojK;ii)Z)ZnH~dKsoK-AXQVzYwWtIKMuNCvP2`%) znKpQ83~Njep@E6u$2Tsk4@Qrom<>m<`)+n-4Ddk-URDk2L@f`~uZ^d5zb3qqn@EUZ zYzqbyvalBf_ZPC`B1)6k6^qOYrcj%6W=^AQFxT={)zA9U4RmpzukmRKRPqy)mo$7TKNpfeudcPgWbcm*oWNKi8hcSf?i@kL72`>wh46pN+RQ``^6L68!qh zG77kkpa_aMpOD+qYtL84Up~FKLIWRFpsuhLXXoj6YWXQeRUb zu~;@Z5rXrW9783 zT05rC+F`$?0TYCLiO?NMmt+(kEm4cxo4o}dw&|(UJSg6?{gT_1zbQxPwezEmvbWkx zQN+K~OeL8wR{VkZkZW>asF5t>pi2t3|uBc4C^U=_2zgv$V zenUyE^He{U4+u!?2 z)JuB7ULX0B5x>Vi*eZyJ`@u^j3e--GAhjjza>Sl-=xEzEqV(Uks6ZslG^ImaJ~D;3 zmJRREMNrLA(i-r!R!EZV9H^Nyl6+;`(ouOPfuF{XK)T+cC@p*@>BY)=yvubdf*AWX zhSc^vDn`-%UR7MN$cOt$ob-kl&w*NF%S3`_GCQ(OKC?bpZX^ofL!YPZTt#!2l#S9*NuSdjO^18{*WZcB1wbi0DCGMv&n$Ec=CcONBL zeV`^WYpFAmvB!z=wrfmSQ|+pyv}Eq7)eJNmxCU^ir&D1A)M>!>9p;(UwMO17+Brc_ znrBu!jeaGI@&1NnUzzq+&$bUEX1o`Z)Hq$)E$O2&z0mHYOMrA#K zxcS|cRwwtgg(PS`4UNTCtJ|H2+Nl*}W~y}eYl{=&U${8L6Dy_2hcSOx-LHgOm^=cR zcx}RFN6vkKcd&ytE?CHpmVlFWtkHY~PB$M9<`ODIu@H9&>$q9ymU-PJ5rRl>fMLat zq~EuMDiFEVA(>ObB7(a*N@glY@o_45I2qU>%F8%9Tb5()DvX^qlceg5o@J}`N0NIE z-4Q#hN>=Yl(=g846|1M|Kbv}=-Hf{QWa#Hn#KnYE(3OY%SOoRZz}7e>UAj~^3*kzh zsOyjWbTC!hJg%zS4HJc=kD`om2Aop~wJT1^7-LmRQuRFIyPD&OI1|)pbefk2&I;VO zP1}vK#tde%RMEBP*6riTmZYIiXQ^b-A`H5%;uA_8h5P7w|zKnvf#xES$-t)4;mCNWJ{Aoofqmb&9XL zeMnBo@W8%nO`~z_f|jL>)--;cekeu1*n`C#OmsXz_cs$;vTYX~TC|%IaU;i$uRq@j z5QC0w!@WIZQ@5}%WWQ?%v1c%)OYf+0q2lcO%AN(JrnrDFHX7eu*>B%j_5b48oTs>v zY6e2Rb}J;DY*pkf>OK&}+3>K$qnlPrgutXmv(Ou#-`aEr01enlrh25fif9}Z9rxX( zEXaJ;Rq6aE>*UOWt~SW7(VF57EaWFO`%U_Bv&}M){IrRnzj~GLal?Vs=Vw>^>3IFD zG^U?&-=$El6OakHnatDMV1L6nXyEO8@`CXe*t)pJP!RnUO|}ddX{~cmhg*}ydozRi z#%(a*%HN(9--hs$skWPZIXURv*(#w&0RJs8`6~QfZ;R_mqCWhyASi(n-j8d+7kTp@ zuiWA8e4jy4fy;YQ{Z3z*`uDobQ%n4ICx0(587e{aWk{+IA9^%Eafn25F!fx&@V_iJ z9*Uf%e_a-rae3ne-mWli|0Z*X@sQ^kE#WzC>t?^L+9GLW_@DHJ;FVd(o#Njb@EWLo1rmOgK$%p~Z@Yh-7BVlJhcW6KN3 z^SbdEN46l1LQPw;$EoxyUK2xs54;5HXkLWy$iM-F6iv7*)WgJ_?wru}eI7(bgS`&o9Vr%bddTNe5 z0URe+#VaQdhVX#Et9_t>0C3-WDFsIqNa7KZapnYPD@us%^6aiUGY10b-HV&+#F&1c z+o)lW%*w*Qyw|J>+W;|OicFw`Ly!WMw&FIQs-`5Gi_kGmjhQ-=47$F%GiGI@G-X;{ zb-;`>LvzKP@g}};3?WI8>1M6AI+Q{^XYPN0p3c8{FkJo_{4%UeSNF&*lUwhS1tQqY^5I}n}lG<1b#xxy6cxakV{>()L3y`s>%597t0ne zMzPP}`LNN}E=J_sEnttqf)|0m56;20=Rj5-k-N9RjdOti2>tV~e=R%y=HIK)li;Zt zDF0#}iIZK=ql<^$0x5enEhJ9X4i_cQyIjLtJv*&s-G$%Ov{_P*L#6;VTML_5);Rq1 zXqS;9OYs_&WiqH(RwDG7CE_2h>GZY^%(GX27nd7!1$mayAwGh0)r6Yb_rlk9kW`ti z*Rr-A^}`?Pu3|9BrRG>aXb{m9W~J87&YmsJa*-DD<}x&ja@*ObNT){-FPfO~V%ojQgptWaW*~Xl#A_!idU)@Or1Yz5YLqV5jJh zM#r++-dMXa`Lm~7p9K&Xi8@dmi^t|D^}ff7esHJESSpGZGSx2@%0>%#IO@PEYaX!5 zugOoy6T%?HX(h=OaV1=mQlo;GbA*YRxmk9t7K^&)<=Tl^F2f!`{Ny-8%BYa_m>4nm zB$fdg8s_D?QhrBzI^0;p>!9;}Yyj~$ybtKMS!3i`YYM`#r7A2D;|k-n%2|e{D@vM1 zu~keKG&ym{*Gm)WY){3WY+qBs2Mw;SGFl)gi2%di)`ry-#6sqZf*V6 z6V>sD2qe9ML%%OGt@!V^17D64k`-r^tGLoZgkJO&IR)*EtWiQ5wE5C~kX~k|0p?%a zUz=G!Yzjbt`Pc+-gS*;(-E1bIW)IX)&E&jL6na%Ah{#sGl3U^Yu7`w#xPWKlxX(_< zh0r+PCNks03vim6ET6%Iplr(8*iPx>@HyX+PpYorlEjm_Ee5tRFHLytXf{6~jwyz? zPYSOaf?{ezM#2OTwaBLx#6ApoZ;fW40^y$hV4N_M1ihKNMoK5v%n@Pg9%uUHF%`3Zwzj1 zG!8uI)!j3n1`)YD8QZQqPBTf`W(k6-FC(oz9->cOvp#I}D>rLj(3w2*x*p$3YjXAp z9ScQ)RYsw;PrLh6@4_eK*PQ3E@%bn}_m$>F2*Wtv?p=qSFXS&^K0ABELV@D_-u2AZbe5r!Q zdh!x6>QWX9eys#jQp7=<(1I=C(Gb0faK%g12T3gtRvb6xny4=ryev$|s-~{4HN2B~ z7phDPY4JAa%mrvmQodD#m#Xc|J=B)&n6BNm`SGqHSLcr zoock7?#pFa?tr^-{bumsW`Quzu%4b@yfk4iq3eDIU9R^gGM6f4F~ zSlUdF6+xR2h_N>r;sBy3U?lFz^tPSb1R$sVxa+#NQwszPnVGpH*1VSZapagm)1a16 z==$M8ijIPZyilqy9tev<%w6^SEH1u55(5(}pF(rSSNU|JS&7k5=!x!pOI6_{wb7Wd zLQ<+D6QkYac*Pusp-6yRLSQ-pnF&ulA+bf04sI^Hr;}&E{b~;(&7b!`6OZRI00)I7 z6KE-dS*cm7`QR}LRS+S-rv^N-CcTUqCjszK_U#OOpLRO3&KoT`Pi%zDC{4#!WbwJj zf?eCvrRaE{9?aM43`Lr9olZq;5n47F*+4YGel3DtKmu#TP7nxa6v(Hj!hHcF@F;q~ z7Q(*Uox=8{!SaT9Xw$A-Bhe!XjqSA!R=L!ls+I+rj#RP)*LL(qXS6S)0Z4+b)CrJ) zP{t4%3F>eUVFH9~dO{Aj3IV}$9uFNX3?WIf9Pp`U$)#-_%S$;-%L$TqwA=(4I{mab zhYWj<3ZDRO_bRu3e_j>CDfChb|9WTo#G~YqbUj5^w!T^!x2N56zz@&l-dt7&UHs|c z!tSkNV$TAx`wa_t#*{^3IV5UvcLj#yoyU(XXjSc#3{}*~-k9imXI0;4X-e_1-pJ{$ zay9-$o+2jE$0rKsYv7sH=^c@MB5O|^L&vW~5pjakd1ebygdQ4;jwcp<_F9)sYd7-- z)V4;^>@(c`hCCFFAO%dJns2<=tQlCqYz#*cG3Qd+yiwyV;W9Brt?-oTmL==Ds+PVz z3fUAqwTVY%sV`W5eP8o>B#E4=T4r^O;YSkLYwhpJ+r!@0`Folf=6gtpOzW*WwLUa& zWlVR|UNdnj)y@{C3V1FFt%d0nLDXn@RURBy70ihsn_;EcPkddbW-yd4LLIr4(R)Tv zr`CDQXi@NzDS@0@LNk=aACk7kF{KOIjNsKuwg*Bnk` zWiZf>f5RfD;V;I&J$N!*1$R@2>llRgA#4m5Xqbuq!xkn-_<8vRcRa$jqyQc0dHJ(L z`B=VeKSRMjSK;^E6VsT|Gtm>T3}}GPYG}-(KI5&!@-w=hwt%zX_$_(w7ic%Xug!@g zKmeMRp6U7HL+`v@yY6EfoB-h(#k9scys`4LOmz30b4-u7Zw_x8H^UqC#q6ebogCxM zOTp(`o-7r+q97g6zd#~Jy9gpydLZr)ODpW~4}Z5D13fMgD<3hlIPdq8Qt#^%TpDq! zUlpzWX3dwwXtBv*7u?%~>>S-*Ue1LGSJ#={g>`OJz6L+TQNU+$PF&Q~CHYKZc^6hf z5EbVL6ZQ1t_+p15p;V{&$lBJ0+SXdz?Pg4vGXX)_H|4aRh1OH_jo}~{$AHI_F%D4R zNxc49b8j2E*Ce0mw%XNJmw-KvX9)opxW`&?E95DzO60Yv=2kq1Gx89rI;p2=v=BYv z60H=9%y;4#BbEkJu~-jp_Ji6np-8FXKb~ZswZwRBla{2@Uz$S!>Gi=7kH&e9hheW= zlGcQV)@zaS{GGZF_1NxLyvOAYl;9Ie zgZeK`ffbZS!BDB|X=JKGYQi9?h;ZGNPil$Uw{ZY>FT){Zckm4IfL#wN=mHmK#>!)mM1JFJeds&eiKTcq=UriUFjJq}@Ac;hmX+ zD(tmSW8DfTJqZmi!V?O}`5E$`^M3NOg zv60%jcFe+49e>Z7*&n+w`&>F0%%N|m5!}K^7@DZd#?+7jD}dDDR4GLqFTU)D)qn+T zT|P?YdTUA|Kgsu@oDOIQo=k5`D75`ST{sxp1e}7`;Nr}>bT9l19q*sFUR_9h@;O3O z*+m~4MjLi;cz&8h;ZDmN!(r{Mu^B>y18?VIFVC1@EvmPoM=|a^vLRvb(LH(QNw4QU z)U2*-{=q%Q9kGW$wXE(TLb-~1r^h9`8Wah6Q0VF|@uLR4yuQ9HBnD^<-f!L7JlNv@ z*%={$8x>Lf-_81iqhPNOISm`d@h&R0ff|FF+7G_(K^eOd_Su*fcEG5yE4Z4O0QM6( zTB!oTOX_mB}^)xoq5P)5wfcb25q)<)N$3 zAPZJ2tBuHTvu(KO!lQ{R6KUh5qcrhdmp35Wx+esy=*S2tEhWW!%@(;=9cJSYO41zT zOp2-{dA_db5;+puJSCWU`(bKfpoGwIz-+mci!5q+!;|sK)U;(>1oGx=QO!D-Ak_#E zl-Ar*5i+JZNi2ohAmR0lg@CY`Y8Xh?ohX*H*Lu{)sAMzQQQ}Y8Bz~kCu5!yH&u^Sv zSJN?a{-lQ^RI-wb6n}AxQ?0$KgU!IlFb*7YUGMVY{r}&)=x6=rf(4pQC3&q@sfJC> zFGJ(xbft-HB~8|ML=+NEF_!}O*1zR&yz>1Af}}i@nVNq4=0J>Ze19QW-$B;PxXQ7e z4Za;+8Le)5GD3R`C^SWJR)WJ^V)Q?{`%cB{+2iKaq96X~^5%7^5U&3y9~KJ^+w|>m z#T}F14uo-CmNf(yH(C9RTvUurj4%dqe!$-7Q~h6;e*T9Y+c$nV`|*#@P?BCrSwcv` zU}B)(ZG`X!yvgtKs2l!`u2nod)Q?Prp0DAlvxRPPY?kDzauM67WD7?+Y0fGBu|X%2 zFcmduOPOEBa*tD@gccCtrQIY)M;p`!t!gb1gYc}NX%>%{5^;BxsDYk4F1$gBoFmXy z>l)AqO_b=hb<$aZGX!^KHX_MaM)6|wUArvf_|QSPGH`k)1dGhAJ|BylA?9{}MRNhc(Z1kXI}(cVaM zHbJR*X=m!LbiATVBvH{^<|;WIJ`k)SXpeGuPlhL!a4)uAvxx@$pL(O-U+|?b7{t#K zA_0X23=9%X@VMZ`NG0XF4ewazd*<$Fhrd?mB$be;HR3|Nz&SjhpdQv4yogd7QH_{> zDVZ^NIlz*t1{k&x0k=OngQ^^4cBUoj~pCqT>V; zWQ|Lwua1VlXGO=usBm*GQbvNH``n8eXv4_Guqh0%GV!-XK2M||o7Hv`9Ul<428 zq>(_{t%eWkbzH{`}rOr8`T*EhP4c3wAC2o|u!LuoobC3hTWP*wV?CV^DdR1+Zsd7(t- zWLmSz;Zz!(4EcT!pEjim?+)wGuh-ru+&sR3YrOPYjVm=QC3xEAkPo#p6UQO5DbF@H zQJgNF+o|V!2IjQ}b#omB*Aq^lf)>|r<1@)ud_wSQ*7cIaQ%16kMkg2R^K~H4H039g z6US@U+%R~&>Y+vnlJor9>y11HY_`HZnQussi^7Mw6~9x)I2Ul!zb-*VZ|N!>_mrc} z16o6*1&zR`#xdn^Zg1AMwSpAO(Nd=J=LscUqGvv!?~vpB^BeDM$@3nXU)y=GS~Q^?u8z_`G4&5WuGIFW+VrYl;4z-Dt(#Sf{TA{kcH~sb zAqo1g9$fKl?}8H>h3`W6I7@a;S|a&19u}$g*brGgDZEOixIAT9*Wg#52`A2*G@`Cb z$%r1DSp})-AL7Wbj7nr?t%^?Jfqq=E`vM1W^Ga`}Kc@qA@;NzAPR19d<5mjqiX`!u zdE9*CSPNT!``9YRmp%L{zZ~-q|9^=ZUZRSYR5&90=ybnD0UW^~a!eF-g2s_3&NMR% zQB_`=`eXejU*ZL;v3yk-_Mi3^l>~zi2z$V-&qz83N66YZP};IyXtc2=Cesu1Xm{`5 zUmbg{TY4ZgPmwHYV4OD>{=-uv3%X~yDZXn;l5O?*pBIM=gO9h(QVPaA4xSB^OM>wx zTm6ab9c6ID{5Vr~;r#ckLNW>gY9p$sOeFh#DW_e{KQJ!7{AMS9JSU7Es5ixAzsGd; zbszYLJLWK7Jgzf1ChE?iOGA>t`fTC;jCnUnFOfc88hy1S^xhfoV2p>Moen#3m6&B5 zkv;Cm_1*y-;9OJypC=3RS&69;3rz4mI(Zd%;M{a?o~3&qNVaa^{2A#Eoe2&FXKbe{ zt_R}J!OUjy2Lz;-%`(W>nUH zZ;z{NQ$L;O_We^Iea$T9l^HRBAk0Fzp785qF$+2R3QVs+eLBZ>F#o% z(t%LvP&~qm$LK@;M(4&LSNZsV|JrxgbK#{ zR8U++Xzs(WXcrqp9i~g422X7V$;*g*MkZ&YR@5LYVWyTz)c~)-fkVQ_dXhAk8eJpu z9%W%HJ@Z`;-IV^&8ZR+#+z4L@yFtC*AA2&YXqMv{hvO9}VNe6GC1lWmsAM4*8pBmN~`d`#9`QLj3R|&`BZNAx-W`=fx_8Kx2z&t%U6INWPx# zhoSp`NWV=&g~0&;awFJoG-hzyd`_+xZM|Ib4>V|$of6u7n`lvvS+khtH>sz=R6Y9B ze7N;-n@&A{#h4-KpWFKGm)U4T>a(Zb3M=IRJVj{6g}I87639>tQ%^%ob0L6A#eAb= zo>*b>d#d4VUFQ`IlfNG7WM1S>q|;9|u9 znGSgrRY9UN_Gmz5&!&rFZPA1pp)aO8*cWcw#bv~Dq$XOy6EII?cw+Ew3_+9MyuaVq z#GzoiE5M5h;{Cm&^TkW&Pf-m5W8hXui11v~E{FNVfB5amEJy(t9(+}>n2)zF>{ZJJ zX8&mhB>&fDg^fvqzOYxrdu{UN6I8vm{iZ&(!N9HbkDr9^%)B_T1cd(zCNCtn_hz54 zimsXWR%_Yy75Qe==7NP$`J$Y;w!Q! z)HdLA?)xoA3p_F}r2Cn20@LI871Nr{oaW&$;dZU3l|sV!M)?4G7`mJn#T;fFyJ6y+ zS&qsMga-~HBtso!(}qsr9Fm;h6v8Hi6r40ZDIUSMYacir1P~f>QSe%8v)>YqrxCXf zJ}J@#N9SX+e8vH|2&;Ww`7sxPL1}Eq=%o}J+8neD;fr7>GCfE^dQ&JKkV?Z^PCpf^R80_z2cCh8#1&T1{46>bnrXsxX$j?s+OWUt|Ahj{7l z8Z7PLXb(o>N9l(gB6Nh4>D|NJKg%5+XDKdN!4y9Cmx$TP?YUJVs7@B;T-hZSiwjJ={QFe9YTGTCIQSgF{o-m7Y1Bm&N#m1k3$ zrXEac50#Xe;<04lEzuoIN7U%pMhXF1q%+xpKxF2Z*SnWx=O)Y-1`6-eh}*f?qi)Mj zc|+(toe95c<v|e^Z_N2LS#$7!3g6Z!_s%&c3uXz>h2v03QQD@K=w|Y!^4W z_*s+d=ygwMf?@s$#R(X~q7epHE$kQz%9q?AhLx-cY=RnTJrxwjG-ZoALxe{l27+3} zU5%(x=20VTBD|D|2vA%DuP zy=>&EaB_p8lq^R0E3iFg$^a>7A;Jb@j!vxNCu9?COM)2}VG`7W36>QA9*v3=Kk6Pn zLEeE^x1odiEET# z)u+8vyC!DM#qfU!MCj1vjd!V}gT|gIK{{J#G_4-%(MV^%?nnilK7LS66 z?5*Vi2gGE*BWwB+M=LtOC1GUYLLzw!rI^`(sn>IJ0(m@yY-UeGPX72yd!FxCflt-MH55!2Zm;6 zop*uzA;22#$|SQJq80+c2DGZx7>>sZ{X+q{=GVT_=35eo{3}KV?d*n?9TMW!WZ4zv zpHX$ch(DDG^69_RXTV@mIFTbr`H+Wr8jn|)AMZ3|tr>Rl`WF$RmP9p^{^8H!ge?5+ zuv*kdDt6?rI%q`{RZMXulvGM-Wy}?8o;W9&Z-IptSatRV8NhVqC zAgLK34*8kbmTnWdJS z7pgSs{cgT7Xmzr{o3i-7|9AxfQMuPN+CUoysp_&qmTW8K$dxBwfkLYkDORFX86)&o zxaxb?T=zphj9h7>JHUdz9&If?t~a?*}Od&0ScJjalnibQfn*%6r| z5{a8D%C1O`$mnS&3V0G8dR8YZra%klx=NH55uhvJ7#qC9wcDad>Gs*VWV z*wgJ9?C(wsuc|f9V@Cb+q$}@ ju)b)xCnMI?=jLl&nh{9C4xu!|6aDOq+o9C?>V{4abCDMN literal 0 HcmV?d00001 diff --git a/docs/_theme/vortex/layout.html b/docs/_theme/vortex/layout.html index 8e0950f9c3b..ab8d7d7719d 100644 --- a/docs/_theme/vortex/layout.html +++ b/docs/_theme/vortex/layout.html @@ -4,14 +4,20 @@ {%- else %} {%- set titlesuffix = "" %} {%- endif %} - +{%- set github = "https://github.com/vortex-data/vortex" %} + - {{- metatags }} + {%- if 'name="viewport"' not in metatags %} + + {%- endif %} {%- block htmltitle %} {{ title|striptags|e }}{{ titlesuffix }} {%- endblock %} + {%- if meta and meta.description %} + + {%- endif %} @@ -22,6 +28,7 @@ {%- for css in css_files %} {{ css_tag(css) }} {%- endfor %} + {%- block scripts %} {%- for js in script_files %} {{ js_tag(js) }} @@ -41,43 +48,61 @@ {%- endif %} {%- block extrahead %}{% endblock %} - -
- -
-
+ diff --git a/docs/_theme/vortex/system/vortex-tokens.css b/docs/_theme/vortex/system/vortex-tokens.css new file mode 100644 index 00000000000..bb38d488f21 --- /dev/null +++ b/docs/_theme/vortex/system/vortex-tokens.css @@ -0,0 +1,110 @@ +/* Vortex design system tokens, copied by tools/site-styles.mjs. + Edit the system, then run tools/site-styles.mjs; do not edit here. + Made by: node tools/site-styles.mjs --sections docs,focus --reads ../vortex.css,../layout.html + sha256 8924c605047ae88eb3667bf4e408e5520327d781e60d3d0906a373f9250ccbc9 */ +:root, [data-theme="dark"] { + --white: #ffffff; + --grey-100: #ebebe8; + --grey-200: #dfdfdd; + --grey-300: #d4d4d2; + --grey-450: #9c9c99; + --grey-500: #646462; + --grey-600: #3c3c3a; + --grey-700: #2b2b29; + --black: #171716; + --blue: #1d0fed; + --blue-soft: #8fa9ff; + --orange: #f27332; + --ground-dark: #0f0f0e; + --ground-light: #f5f5f2; + --ground: var(--ground-dark); + --surface: var(--black); + --raised: var(--grey-700); + --line: var(--grey-700); + --edge: var(--grey-600); + --wash: rgba(43, 43, 41, 0.5); + --ink: var(--white); + --ink-body: var(--grey-300); + --ink-muted: var(--grey-450); + --accent-ink: var(--blue-soft); + --accent-soft: rgba(47, 99, 255, 0.16); + --focus-ring: var(--accent-ink); + --warn-ink: var(--orange); + --syntax-plain: var(--grey-300); + --syntax-keyword: var(--blue-soft); + --syntax-function: #f4df9a; + --syntax-type: #6fcfd6; + --syntax-string: #f2a0c2; + --syntax-comment: var(--grey-450); + color-scheme: dark; +} +[data-theme="light"] { + --ground: var(--ground-light); + --surface: var(--white); + --raised: var(--grey-100); + --line: var(--grey-200); + --edge: var(--grey-300); + --wash: rgba(235, 235, 232, 0.5); + --ink: var(--black); + --ink-body: var(--grey-600); + --ink-muted: var(--grey-500); + --accent-ink: var(--blue); + --accent-soft: rgba(29, 15, 237, 0.1); + --focus-ring: var(--accent-ink); + --warn-ink: #ad4a00; + --syntax-plain: var(--grey-600); + --syntax-keyword: var(--blue); + --syntax-function: #6d24b5; + --syntax-type: #0a6b75; + --syntax-string: #a8205b; + --syntax-comment: var(--grey-500); + color-scheme: light; +} +@media (prefers-color-scheme: light) { + [data-theme="system"] { + --ground: var(--ground-light); + --surface: var(--white); + --raised: var(--grey-100); + --line: var(--grey-200); + --edge: var(--grey-300); + --wash: rgba(235, 235, 232, 0.5); + --ink: var(--black); + --ink-body: var(--grey-600); + --ink-muted: var(--grey-500); + --accent-ink: var(--blue); + --accent-soft: rgba(29, 15, 237, 0.1); + --focus-ring: var(--accent-ink); + --warn-ink: #ad4a00; + --syntax-plain: var(--grey-600); + --syntax-keyword: var(--blue); + --syntax-function: #6d24b5; + --syntax-type: #0a6b75; + --syntax-string: #a8205b; + --syntax-comment: var(--grey-500); + color-scheme: light; + } +} +:root { + --space-4: 4px; + --space-8: 8px; + --space-12: 12px; + --space-16: 16px; + --space-24: 24px; + --space-32: 32px; + --space-48: 48px; + --radius-control: 4px; + --radius-card: 6px; + --page-max: 1440px; + --measure: 54ch; + --page-pad: 48px; + --control-md: 32px; + --control-sm: 28px; + --vx-motion: 150ms ease-out; + --font-sans: Geist, system-ui, sans-serif; + --font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Cascadia Mono", "Liberation Mono", monospace; +} +@media (max-width: 640px) { + :root { + --page-pad: 16px; + } +} diff --git a/docs/_theme/vortex/system/vortex.css b/docs/_theme/vortex/system/vortex.css new file mode 100644 index 00000000000..c6774d765d8 --- /dev/null +++ b/docs/_theme/vortex/system/vortex.css @@ -0,0 +1,249 @@ +/* Vortex design system components (sections: docs, focus), copied by tools/site-styles.mjs. + Edit the system, then run tools/site-styles.mjs; do not edit here. + Made by: node tools/site-styles.mjs --sections docs,focus --reads ../vortex.css,../layout.html + sha256 a7fe16d56c1e334cfc98bb199ac04e2ff8cc1b0c61af3e26e8fd4784da04e2b2 */ + +/* ------------------------------------------------------------------ docs + docs.vortex.dev: Sphinx's own markup, drawn with no script and no web font. The theme writes + vx-docs on , data-theme="system" on (so the tokens follow the OS without a script), + and the frame's classes below. Everything inside the nav, the rail and main is what Sphinx + writes. Type is in rem (1rem is 16px), so text follows the reader's default size. */ +.vx-docs { margin: 0; background: var(--ground); color: var(--ink-body); font: 400 1rem/1.6 var(--font-sans); overflow-x: clip; } +.vx-docs *, .vx-docs ::before, .vx-docs ::after { box-sizing: border-box; } +.vx-docs svg { width: 16px; height: 16px; flex: none; fill: currentColor; } +.vx-docs summary { list-style: none; cursor: pointer; } +.vx-docs summary::-webkit-details-marker { display: none; } +.vx-docs-skip { position: absolute; z-index: 9; left: var(--space-8); top: -6rem; padding: var(--space-8) var(--space-12); background: var(--surface); color: var(--ink); border: 1px solid var(--edge); border-radius: var(--radius-control); } +.vx-docs-skip:focus { top: var(--space-8); } +/* The phone menu's checkbox: hidden, with its label in the header. It takes focus only where the + menu shows. */ +.vx-docs-toggle { display: none; position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } +.vx-docs-label, +.vx-docs-toc p, +.vx-docs-fold > summary, +.vx-docs-nav .toctree-l1 > a, +.vx-docs-main p.rubric, +.vx-docs-main .highlight::before { font: 500 0.75rem/1.3 var(--font-mono); color: var(--ink-muted); } + +/* The header: Header's docs variant, solid, with a search form in place of the Ctrl K button. */ +.vx-docs-header { display: flex; align-items: center; gap: var(--space-24); height: 64px; padding: 0 var(--page-pad); border-bottom: 1px solid var(--line); } +.vx-docs-brand { display: flex; align-items: center; gap: var(--space-12); color: var(--ink); text-decoration: none; } +.vx-docs-brand img { display: block; width: auto; height: 24px; } +.vx-docs-brand .vx-docs-label { padding-left: var(--space-12); border-left: 1px solid var(--edge); line-height: 16px; } +.vx-docs-search { position: relative; display: flex; align-items: center; width: 360px; margin: 0 auto; color: var(--ink-muted); } +.vx-docs-search svg { position: absolute; left: var(--space-12); pointer-events: none; } +.vx-docs-search input { width: 100%; height: var(--control-md); padding: 0 var(--space-12) 0 calc(var(--space-12) + 16px + var(--space-8)); background: var(--raised); border: 1px solid var(--edge); border-radius: var(--radius-control); color: var(--ink); font: 400 0.875rem var(--font-sans); transition: border-color var(--vx-motion); } +.vx-docs-search input:hover { border-color: var(--ink); } +.vx-docs-search input::placeholder { color: var(--ink-muted); opacity: 1; } +.vx-docs-links { display: flex; align-items: center; gap: var(--space-16); margin-left: auto; font: 500 0.75rem/1.3 var(--font-mono); } +.vx-docs-links a { display: flex; color: var(--ink-muted); text-decoration: none; transition: color var(--vx-motion); } +.vx-docs-links a:hover { color: var(--ink); } +.vx-docs-menu, .vx-docs-links .is-narrow { display: none; } +.vx-docs-menu, .vx-docs-links .is-narrow { place-items: center; width: var(--control-md); height: var(--control-md); border: 1px solid var(--edge); border-radius: var(--radius-control); color: var(--ink-body); cursor: pointer; } + +/* The frame: a 260px nav, the page column on surface, and a 220px "On this page" rail. The page + column holds the folded contents, main, and the links under it. Only main is + role="main", which Sphinx search reads for its snippets. */ +.vx-docs-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr) 220px; max-width: var(--page-max); margin: 0 auto; padding: 0 var(--page-pad); } +.vx-docs-nav, .vx-docs-toc { grid-row: 1; position: sticky; top: 0; align-self: start; max-height: 100vh; overflow-y: auto; } +.vx-docs-page { grid-column: 2; grid-row: 1; min-width: 0; padding: var(--space-32) var(--space-32) var(--space-48); background: var(--surface); border-inline: 1px solid var(--line); } +.vx-docs-main { font-size: 1rem; line-height: 1.6; text-wrap: pretty; } +.vx-docs-toc { grid-column: 3; padding: var(--space-16) 0 var(--space-32) var(--space-24); } + +/* DocsNav: the toctree. Top-level pages are the mono group labels; the current page is a raised fill. */ +.vx-docs-nav { padding: var(--space-8) var(--space-12) var(--space-32) 0; font-size: 0.875rem; line-height: 1.3; } +.vx-docs-nav ul, .vx-docs-toc ul { list-style: none; margin: 0; padding: 0; } +.vx-docs-nav li { position: relative; } +.vx-docs-nav ul ul { padding-left: var(--space-12); } +.vx-docs-nav .toctree-l1 { margin-top: var(--space-4); } +.vx-docs-nav a { display: flex; align-items: center; min-height: var(--control-sm); padding: 2px var(--space-32) 2px var(--space-8); border-radius: var(--radius-control); font: 400 0.8125rem/1.3 var(--font-mono); color: var(--ink-body); text-decoration: none; transition: background-color var(--vx-motion), color var(--vx-motion); } +.vx-docs-nav a:hover, .vx-docs-nav summary:hover { background: var(--wash); color: var(--ink); } +.vx-docs-nav a:active { background: var(--edge); } +.vx-docs-nav a[aria-current="page"] { background: var(--raised); color: var(--ink); font-weight: 500; } +.vx-docs-nav summary { position: absolute; top: 0; right: 0; width: var(--control-sm); height: var(--control-sm); border-radius: var(--radius-control); color: var(--ink-muted); } +.vx-docs-nav summary::after, +.vx-docs-fold > summary::after { content: ""; position: absolute; inset: var(--space-8); background: currentColor; mask: url("data:image/svg+xml,") center / contain no-repeat; transition: rotate var(--vx-motion); } +.vx-docs-nav details[open] > summary::after, +.vx-docs-fold[open] > summary::after { rotate: 90deg; } + +/* Toc: the page's sections on a line rail. With no scroll spy there is no current tick. */ +.vx-docs-toc p { margin: 0 0 var(--space-12); } +.vx-docs-toc ul ul { padding-left: var(--space-12); } +.vx-docs-toc nav > ul { border-left: 1px solid var(--line); } +.vx-docs-toc a, .vx-docs-fold a { display: block; margin-left: -1px; padding: 3px 0 3px var(--space-16); font: 400 0.8125rem/1.4 var(--font-mono); color: var(--ink-muted); text-decoration: none; overflow-wrap: anywhere; transition: color var(--vx-motion); } +.vx-docs-toc a:hover, .vx-docs-fold a:hover { color: var(--ink); } +/* The same contents, folded above the page on screens too narrow for the rail. */ +.vx-docs-fold { display: none; margin: 0 0 var(--space-24); border: 1px solid var(--line); border-radius: var(--radius-card); } +.vx-docs-fold > summary { position: relative; padding: var(--space-8) var(--space-12); line-height: 20px; } +.vx-docs-fold > summary::after { inset: var(--space-12) var(--space-12) auto auto; width: 16px; height: 16px; } +.vx-docs-fold ul { list-style: none; margin: 0 var(--space-12) var(--space-12); padding: 0; } +.vx-docs-fold ul ul { margin: 0; padding-left: var(--space-12); } + +/* Previous and next, and the edit link. The docs draw no breadcrumb: the sidebar shows the path. */ +.vx-docs-pager { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-16); margin-top: var(--space-48); } +.vx-docs-pager a { padding: var(--space-12) var(--space-16); border: 1px solid var(--line); border-radius: var(--radius-card); font: 500 0.875rem/1.4 var(--font-mono); color: var(--ink); text-decoration: none; transition: background-color var(--vx-motion); } +.vx-docs-pager a:hover { background: var(--wash); } +.vx-docs-pager a:active { background: var(--edge); } +.vx-docs-pager .vx-docs-label { display: block; margin-bottom: var(--space-4); } +.vx-docs-pager .is-next { grid-column: 2; text-align: right; } +.vx-docs-edit { margin: var(--space-32) 0 0; font-size: 0.875rem; } +.vx-docs-edit a { color: var(--ink-muted); transition: color var(--vx-motion); } +.vx-docs-edit a:hover { color: var(--ink); } + +/* The footer: Footer, solid. */ +.vx-docs-footer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-16) var(--space-24); padding: var(--space-16) var(--page-pad); border-top: 1px solid var(--line); font: 400 0.6875rem/1.5 var(--font-mono); letter-spacing: 0.02em; color: var(--ink-muted); } +.vx-docs-footer p { flex: 1 1 420px; min-width: 0; margin: 0; text-wrap: balance; } +.vx-docs-footer nav { display: flex; flex-wrap: wrap; gap: var(--space-8) var(--space-16); } +.vx-docs-footer a { color: inherit; text-decoration: none; } +.vx-docs-footer a:hover { color: var(--ink); } +/* A link in the footer's text is underlined, as vx-link draws it, so colour is not its only mark. */ +.vx-docs-footer p a { color: var(--ink); text-decoration: underline; text-decoration-thickness: 1px; text-decoration-color: color-mix(in srgb, var(--ink) 30%, transparent); text-underline-offset: 3px; } + +/* The reading column. :where() keeps the flow margins below any element's own. */ +.vx-docs-main :where(section > *, dd > *, .admonition > *) { margin: 0 0 var(--space-16); } +.vx-docs-main :where(section > :is(p, ul, ol, blockquote, dl.simple)) { max-width: var(--measure); } +.vx-docs-main li + li { margin-top: var(--space-4); } +.vx-docs-main li::marker { color: var(--ink-muted); } +/* A post's mono heading ladder, one level up: a docs page's h1 is a post's h2. */ +.vx-docs-main :is(h1, h2, h3, h4) { scroll-margin-top: var(--space-16); color: var(--ink); text-wrap: balance; } +.vx-docs-main h1 { margin: 0 0 var(--space-24); font: 500 1.75rem/1.15 var(--font-mono); letter-spacing: -0.02em; } +.vx-docs-main h2 { margin: var(--space-32) 0 var(--space-8); font: 600 1.375rem/1.2 var(--font-mono); letter-spacing: -0.02em; } +.vx-docs-main h3 { margin: var(--space-24) 0 var(--space-8); font: 600 1.125rem/1.25 var(--font-mono); letter-spacing: -0.01em; } +.vx-docs-main h4 { margin: var(--space-16) 0 var(--space-4); font: 600 1rem/1.25 var(--font-mono); letter-spacing: -0.01em; } +.vx-docs-main strong { font-weight: 600; color: var(--ink); } +/* Prose links are Blue at rest, as the old docs theme's were: a reference page is mostly links, and the reader scans for them. */ +.vx-docs-main a { color: var(--accent-ink); text-decoration: underline 1px color-mix(in srgb, var(--accent-ink) 40%, transparent); text-underline-offset: 3px; transition: color var(--vx-motion), text-decoration-color var(--vx-motion); } +.vx-docs-main a:hover { text-decoration-color: currentColor; } +.vx-docs-main a:has(> code) { text-decoration: none; } +.vx-docs-main hr { margin: var(--space-32) 0; border: 0; border-top: 1px solid var(--line); } +.vx-docs-main blockquote { padding-left: var(--space-16); border-left: 2px solid var(--edge); } +.vx-docs-main img { max-width: 100%; height: auto; } +.vx-docs-main figcaption { margin-top: var(--space-8); font-size: 0.875rem; line-height: 1.5; color: var(--ink-muted); } +.vx-docs-main figcaption p { margin: 0; } +.vx-docs-main p.rubric { margin: var(--space-24) 0 var(--space-8); } +.vx-docs-main a.headerlink { display: inline-flex; margin-left: var(--space-8); vertical-align: middle; font-size: 0; opacity: 0; transition: opacity var(--vx-motion); } +.vx-docs-main a.headerlink::before { content: ""; width: 16px; height: 16px; background: var(--ink-muted); mask: url("data:image/svg+xml,") center / contain no-repeat; } +.vx-docs-main :hover > a.headerlink, .vx-docs-main a.headerlink:focus-visible { opacity: 1; } + +/* Inline code: CodeBlock's inline code. */ +.vx-docs-main code.literal { padding: 1px var(--space-4); font: 400 0.875em/1 var(--font-mono); color: var(--ink); background: var(--raised); border: 1px solid var(--edge); border-radius: var(--radius-control); overflow-wrap: anywhere; -webkit-box-decoration-break: clone; box-decoration-break: clone; transition: border-color var(--vx-motion); } +.vx-docs-main a:hover > code.literal { border-color: var(--ink); } +/* Code that links (an API name in prose) takes the syntax type colour, as in signatures, and an underline, + so colour is not the only thing that marks it apart from plain inline code (WCAG 1.4.1). */ +.vx-docs-main a.reference > code.xref { color: var(--syntax-type); text-decoration: underline 1px color-mix(in srgb, currentColor 40%, transparent); text-underline-offset: 2px; } +.vx-docs-main a.reference:hover > code.xref { text-decoration-color: currentColor; } + +/* CodeBlock: Pygments' classes on the four syntax tokens, under a label from highlight-. */ +.vx-docs-main div[class*="highlight-"] > .highlight { background: var(--raised); border-radius: var(--radius-card); overflow: hidden; } +.vx-docs-main :where(div[class*="highlight-"]) > .highlight::before { content: "Text"; display: block; padding: var(--space-4) var(--space-16); border-bottom: 1px solid var(--edge); line-height: 20px; } +.vx-docs-main :is(.highlight-python, .highlight-pycon) > .highlight::before { content: "Python"; } +.vx-docs-main :is(.highlight-bash, .highlight-sh, .highlight-shell, .highlight-console) > .highlight::before { content: "Shell"; } +.vx-docs-main .highlight-rust > .highlight::before { content: "Rust"; } +.vx-docs-main .highlight-sql > .highlight::before { content: "SQL"; } +.vx-docs-main .highlight-java > .highlight::before { content: "Java"; } +.vx-docs-main .highlight-kotlin > .highlight::before { content: "Kotlin"; } +.vx-docs-main .highlight-c > .highlight::before { content: "C"; } +.vx-docs-main .highlight-cpp > .highlight::before { content: "C++"; } +.vx-docs-main .highlight-json > .highlight::before { content: "JSON"; } +.vx-docs-main .highlight-toml > .highlight::before { content: "TOML"; } +.vx-docs-main .highlight-xml > .highlight::before { content: "XML"; } +.vx-docs-main .highlight-cmake > .highlight::before { content: "CMake"; } +.vx-docs-main .highlight-protobuf > .highlight::before { content: "Protobuf"; } +.vx-docs-main .highlight pre { margin: 0; padding: var(--space-12) var(--space-16); overflow-x: auto; font: 400 0.8125rem/1.5 var(--font-mono); color: var(--syntax-plain); tab-size: 4; } +.vx-docs-main .highlight :is(.k, .kn, .kd, .kr, .kp, .ow, .nt) { color: var(--syntax-keyword); } +.vx-docs-main .highlight :is(.nf, .fm, .nb, .bp, .nd) { color: var(--syntax-function); } +.vx-docs-main .highlight :is(.nc, .ne, .nn, .kt) { color: var(--syntax-type); } +.vx-docs-main .highlight :is(.s, .s1, .s2, .sa, .sb, .sc, .sd, .se, .sh, .si, .sx, .m, .mi, .mf, .mh, .mo, .il, .kc) { color: var(--syntax-string); } +.vx-docs-main .highlight :is(.c, .c1, .cm, .ch, .cs, .cp, .gp) { color: var(--syntax-comment); } +.vx-docs-main .highlight :is(.c, .c1, .cm, .ch, .cs) { font-style: italic; } + +/* Callout: an admonition. The icon carries the kind: info, or warning for what can go wrong. */ +.vx-docs-main .admonition { position: relative; padding: var(--space-12) var(--space-16) var(--space-12) var(--space-48); background: var(--raised); border-radius: var(--radius-card); font-size: 0.875rem; line-height: 1.55; } +.vx-docs-main .admonition::before { content: ""; position: absolute; left: var(--space-16); top: 13px; width: 20px; height: 20px; background: var(--ink); mask: url("data:image/svg+xml,") center / contain no-repeat; } +.vx-docs-main :is(.warning, .caution, .attention, .danger, .error)::before { mask-image: url("data:image/svg+xml,"); background: var(--warn-ink); } +.vx-docs-main .admonition > * { margin-bottom: var(--space-8); } +.vx-docs-main .admonition > :last-child { margin-bottom: 0; } +.vx-docs-main p.admonition-title { margin-bottom: 2px; font-weight: 600; color: var(--ink); } +/* A warning's title takes warn-ink, as its icon does, so a warning does not read as a note. No rule, no tint. */ +.vx-docs-main :is(.warning, .caution, .attention, .danger, .error) > p.admonition-title { color: var(--warn-ink); } + +/* Tables: the tables section's heads and hairline rows. */ +.vx-docs-main table.docutils { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; font-size: 0.875rem; line-height: 1.4; font-variant-numeric: tabular-nums; } +.vx-docs-main table.docutils th { padding: calc(var(--space-12) / 2) var(--space-12) calc(var(--space-12) / 2) 0; border-bottom: 1px solid var(--edge); text-align: left; vertical-align: bottom; font: 500 0.6875rem/1.3 var(--font-mono); color: var(--ink-muted); } +.vx-docs-main table.docutils td { padding: calc(var(--space-12) / 2) var(--space-12) calc(var(--space-12) / 2) 0; border-bottom: 1px solid var(--line); vertical-align: top; } +/* An API summary table keeps each name whole above 760px. Inline code may break anywhere, and the + table's auto layout used that to squeeze the first column. A phone keeps the wrap, so nothing scrolls. */ +@media (min-width: 761px) { + .vx-docs-main table.autosummary td:first-child code { overflow-wrap: normal; } +} +.vx-docs-main :is(td, th) > p { margin: 0; } + +/* The API reference: a signature on raised, then its description and fields. */ +.vx-docs-main :is(dl.py, dl.c) { margin: var(--space-32) 0; } +.vx-docs-main :is(dl.py, dl.c) :is(dl.py, dl.c) { margin: var(--space-24) 0; } +.vx-docs-main dt.sig { padding: var(--space-8) var(--space-12); background: var(--raised); border-radius: var(--radius-control); font: 400 0.875rem/1.6 var(--font-mono); color: var(--syntax-plain); overflow-wrap: anywhere; } +.vx-docs-main dt.sig .sig-name { font-weight: 600; color: var(--ink); } +.vx-docs-main dt.sig :is(.sig-prename, .sig-paren) { color: var(--ink-muted); } +.vx-docs-main dt.sig .property, .vx-docs-main dt.sig .k { font-style: normal; color: var(--syntax-keyword); } +.vx-docs-main dt.sig em { font-style: normal; } +.vx-docs-main dt.sig a { text-decoration: none; } +/* Types read apart from names and punctuation, as in a code block. */ +.vx-docs-main dt.sig :is(a.reference, .sig-return-typehint) { color: var(--syntax-type); } +.vx-docs-main dt.sig a.reference:hover { text-decoration: underline; } +.vx-docs-main :is(dl.py, dl.c) > dd { margin: var(--space-12) 0 0 var(--space-24); } +.vx-docs-main dl.field-list { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--space-8) var(--space-24); font-size: 1rem; } +/* The labels (Parameters, Return type) are ink in the headings' voice, so each field reads as a group. */ +.vx-docs-main dl.field-list > dt { padding-top: var(--space-4); font: 600 0.875rem/1.5 var(--font-mono); color: var(--ink); } +.vx-docs-main dl.field-list > dt .colon { display: none; } +.vx-docs-main dl.field-list > dd { margin: 0; } +.vx-docs-main dl.field-list > dd > :last-child { margin-bottom: 0; } +/* A linked type is a type, as in the signature, not an inline code chip: a row of chips reads as boxes. */ +.vx-docs-main dl.field-list a.reference > code.xref { padding: 0; background: none; border: 0; color: var(--syntax-type); } +.vx-docs-main dl.field-list a.reference:hover > code.xref { text-decoration: underline; } + +/* Footnotes, the indexes, and search. */ +.vx-docs-main aside.footnote { display: flex; gap: var(--space-8); font-size: 0.875rem; color: var(--ink-muted); } +.vx-docs-main aside.footnote > p { margin: 0; } +.vx-docs-main table.indextable { width: 100%; border-collapse: collapse; font-size: 0.875rem; } +.vx-docs-main table.indextable td { padding: var(--space-4) var(--space-16) var(--space-4) 0; vertical-align: top; } +.vx-docs-main table.indextable ul { margin: 0; padding: 0; list-style: none; } +.vx-docs-main table.indextable tr.cap td { padding-top: var(--space-24); font: 500 0.75rem/1.3 var(--font-mono); color: var(--ink-muted); } +.vx-docs-main :is(.genindex-jumpbox, .modindex-jumpbox) { font-family: var(--font-mono); overflow-wrap: anywhere; } +.vx-docs-main form:not([role]) { display: flex; gap: var(--space-8); } +.vx-docs-main form:not([role]) input { height: var(--control-md); padding: 0 var(--space-12); background: var(--raised); border: 1px solid var(--edge); border-radius: var(--radius-control); color: var(--ink); font: 400 0.875rem var(--font-sans); } +.vx-docs-main form:not([role]) input[type="text"] { flex: 1; min-width: 0; } +.vx-docs-main form:not([role]) input[type="submit"] { cursor: pointer; } +.vx-docs-main ul.search { padding: 0; list-style: none; } +.vx-docs-main ul.search > li { padding: var(--space-16) 0; border-bottom: 1px solid var(--line); } +.vx-docs-main ul.search p.context { margin: var(--space-4) 0 0; font-size: 0.875rem; color: var(--ink-muted); } +.vx-docs-main span.highlighted { background: var(--accent-soft); color: var(--ink); } + +@media (max-width: 1120px) { + .vx-docs-layout { grid-template-columns: 260px minmax(0, 1fr); } + .vx-docs-layout > .vx-docs-toc { display: none; } + .vx-docs-fold { display: block; } +} +@media (max-width: 760px) { + .vx-docs-layout { display: block; padding: 0; } + .vx-docs-layout > .vx-docs-nav { display: none; position: static; max-height: none; padding: var(--space-8) var(--page-pad) var(--space-24); border-bottom: 1px solid var(--line); } + .vx-docs-toggle { display: block; } + .vx-docs-toggle:checked ~ .vx-docs-layout .vx-docs-nav { display: block; } + .vx-docs-search { display: none; } + .vx-docs-menu, .vx-docs-links .is-narrow { display: grid; } + .vx-docs-page { padding: var(--space-24) var(--page-pad) var(--space-32); border: 0; } + .vx-docs-main dl.field-list { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); } + .vx-docs-main table.genindextable, .vx-docs-main table.genindextable :is(tbody, tr, td) { display: block; } +} +@media (max-width: 640px) { + .vx-docs-header { gap: var(--space-16); } + .vx-docs-brand .vx-docs-label, .vx-docs-links .is-wide { display: none; } + .vx-docs-links { gap: var(--space-12); } +} + +/* ------------------------------------------------------------------ focus + One rule for every focusable element, last so that it wins over the resets above. The docs' + phone menu is a hidden checkbox, so its label shows the checkbox's ring. Rows and + segments inside a clipped container draw it inside their edge. */ +:is(a, button, input, select, textarea, summary, [tabindex]):focus-visible, +.vx-docs-toggle:focus-visible ~ .vx-docs-header .vx-docs-menu { outline: 2px solid var(--focus-ring); outline-offset: 2px; } +:is(.vx-docs-nav a, .vx-docs-nav summary, .vx-docs-toc a, .vx-docs-fold a, .vx-post-row, .vx-tree-node, .vx-tabs-list button, .vx-switch button, .vx-theme-switch button, .vx-table-wrap :is(a, button), .vx-docs-main table.docutils a):focus-visible { outline-offset: -2px; } diff --git a/docs/_theme/vortex/vortex.css b/docs/_theme/vortex/vortex.css index 0b0d9c92d22..d803f688f4a 100644 --- a/docs/_theme/vortex/vortex.css +++ b/docs/_theme/vortex/vortex.css @@ -1,308 +1,56 @@ -:root { - --bg: #ffffff; - --bg-alt: #f5f6f7; - --fg: #101828; - --fg-muted: #4a5565; - --link: #0e7490; - --border: #e5e7eb; - --accent: #0e7490; - --warn: #9a6700; - --danger: #a40e26; - --mark: #fff8c5; - color-scheme: light dark; +/* The docs' own rules. The build joins them after the design system's copies in system/: the + tokens, and the system's docs rules for the frame and Sphinx's markup. Spacing, colour, radii, + and motion come from the tokens. */ + +/* Geist Mono, the tokens' first mono face: Google Fonts' Latin subset, one variable file for every + weight (23 KB). layout.html preloads it. Body text keeps the platform's sans. */ +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url("fonts/geist-mono-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } + @media (prefers-color-scheme: dark) { - :root { - --bg: #101010; - --bg-alt: #1a1a1a; - --fg: #ececec; - --fg-muted: #99a1af; - --link: #2cb9d1; - --border: #272727; - --accent: #2cb9d1; - --warn: #d29922; - --danger: #ff8182; - --mark: #693e00; - } .only-light { display: none !important; } } @media (prefers-color-scheme: light) { .only-dark { display: none !important; } } -* { box-sizing: border-box; } -body { - margin: 0; - background: var(--bg); - color: var(--fg); - font: 1rem/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -} -img, svg { max-width: 100%; height: auto; } -a { color: var(--link); text-decoration: none; } -a:hover { text-decoration: underline; } -:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } - -h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin: 1.5em 0 0.5em; } -h1 { font-size: 1.75rem; margin-top: 0.5em; } -h2 { font-size: 1.35rem; } -h3 { font-size: 1.15rem; } -.visually-hidden { +/* The landing page: the wordmark in place of the title, then cards. */ +.page-index .vx-docs-main > section > h1 { position: absolute; - width: 1px; height: 1px; - margin: -1px; padding: 0; + width: 1px; + height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } -.skip-link { - position: absolute; - left: 0.5rem; top: -10rem; - z-index: 3; - padding: 0.5rem 1rem; - background: var(--bg-alt); -} -.skip-link:focus { top: 0.5rem; } - -.header { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.5rem 1rem; - border-bottom: 1px solid var(--border); -} -.logo { - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--fg); - font-weight: 600; - font-size: 1.1rem; -} -.search { flex: 1; max-width: 24rem; } -.search input { - width: 100%; - padding: 0.3rem 0.6rem; - border: 1px solid var(--border); - border-radius: 0.4rem; - background: var(--bg); - color: var(--fg); - font: inherit; -} -.extlinks { - display: flex; - align-items: center; - gap: 0.75rem; - margin-left: auto; -} -.extlinks a { color: var(--fg-muted); } -.extlinks svg { display: block; } -.nav-toggle { display: none; color: var(--fg-muted); cursor: pointer; } -#nav-toggle:focus-visible ~ .header .nav-toggle { - outline: 2px solid var(--accent); -} - -.layout { - display: flex; - gap: 2rem; - max-width: 90rem; - margin: 0 auto; - padding: 0 1rem; -} -.sidebar { - flex: 0 0 16rem; - position: sticky; - top: 0; - max-height: 100vh; - overflow-y: auto; - padding: 1rem 0.5rem 2rem 0; - border-right: 1px solid var(--border); - font-size: 0.9rem; -} -.content { - flex: 1; - min-width: 0; - max-width: 48rem; - padding: 1rem 0 3rem; -} -.pagetoc { - flex: 0 0 13rem; - position: sticky; - top: 0; - max-height: 100vh; - overflow-y: auto; - padding: 1rem 0; - font-size: 0.85rem; -} -.pagetoc > nav > p { margin: 0 0 0.25rem; font-weight: 600; } -.pagetoc ul { list-style: none; margin: 0; padding-left: 0; } -.pagetoc ul ul { padding-left: 0.9rem; } -.pagetoc a { color: var(--fg-muted); display: inline-block; padding: 0.1rem 0; } -.pagetoc a:hover { color: var(--link); } - -.sidebar ul { list-style: none; margin: 0; padding-left: 0; } -.sidebar ul ul { padding-left: 0.9rem; } -.sidebar li { position: relative; } -.sidebar a { - display: block; - padding: 0.15rem 1.6rem 0.15rem 0.4rem; - border-radius: 0.3rem; - color: var(--fg-muted); -} -.sidebar a:hover { color: var(--link); text-decoration: none; } -.sidebar a.current { - color: var(--accent); - font-weight: 600; - background: var(--bg-alt); -} -.sidebar summary { - position: absolute; - top: 0.15rem; right: 0.2rem; - width: 1.3rem; - list-style: none; - cursor: pointer; - text-align: center; - color: var(--fg-muted); - user-select: none; -} -.sidebar summary::-webkit-details-marker { display: none; } -.sidebar summary::after { content: "➤"; display: inline-block; } -.sidebar details[open] > summary::after { transform: rotate(90deg); } - -.footer { - margin-top: 3rem; - padding-top: 1rem; - border-top: 1px solid var(--border); - font-size: 0.85rem; - color: var(--fg-muted); -} - -pre, code, .sig { - font-family: monospace; - font-size: 0.875em; -} -pre { - padding: 0.75rem 1rem; - overflow-x: auto; - line-height: 1.45; -} -[class*="highlight-"] { margin: 1rem 0; } -.highlight pre { margin: 0; border: 1px solid var(--border); border-radius: 0.4rem; } -.literal { - background: var(--bg-alt); - padding: 0.1em 0.3em; - border-radius: 0.3em; - overflow-wrap: break-word; -} -dl.py, dl.c { margin: 1.5rem 0; } -.sig { - padding: 0.4rem 0.6rem; - background: var(--bg-alt); - border-left: 3px solid var(--accent); - border-radius: 0 0.3rem 0.3rem 0; - overflow-x: auto; -} -.sig .sig-name { font-weight: 700; } -.sig + dd, .field-list + * { margin-top: 0.5rem; } -dd { margin-left: 1.5rem; } -.field-list { display: grid; grid-template-columns: max-content auto; gap: 0.25rem 1rem; } -.field-list > dt { font-weight: 600; } -.field-list > dd { margin: 0; } -.property { color: var(--fg-muted); font-style: normal; } - -a.headerlink { - visibility: hidden; - margin-left: 0.3rem; - color: var(--fg-muted); - font-size: 0.8em; -} -h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, -h4:hover > a.headerlink, dt:hover > a.headerlink { - visibility: visible; -} -a.headerlink:focus-visible { - visibility: visible; -} - -.admonition { - margin: 1rem 0; - padding: 0.5rem 1rem; - border: 1px solid var(--border); - border-left: 3px solid var(--accent); - border-radius: 0.3rem; - background: var(--bg-alt); - font-size: 0.95rem; -} -.admonition > .admonition-title { font-weight: 600; margin: 0 0 0.25rem; } -.admonition > :last-child { margin-bottom: 0; } -.warning { border-left-color: var(--warn); } -.warning > .admonition-title { color: var(--warn); } -.danger, .error { border-left-color: var(--danger); } -.danger > .admonition-title, .error > .admonition-title { color: var(--danger); } - -table.docutils { - border-collapse: collapse; - display: block; - overflow-x: auto; - margin: 1rem 0; -} -table.docutils th, table.docutils td { - border: 1px solid var(--border); - padding: 0.3rem 0.75rem; - text-align: left; -} -table.docutils th { background: var(--bg-alt); } - -table.versioning-terms p { margin: 0; } -table.versioning-terms td:first-child { white-space: nowrap; } -figure.versioning-diagram { margin-inline: 0; } - -.page-index .content > section > h1 { - position: absolute; - width: 1px; height: 1px; - margin: -1px; - overflow: hidden; - clip-path: inset(50%); -} -.vortex-wordmark { display: block; max-width: min(400px, 100%); margin: 1.5rem auto 2.5rem; } +.vx-docs-main .vortex-wordmark { display: block; max-width: min(400px, 100%); margin: var(--space-24) auto var(--space-48); } .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); - gap: 1rem; + gap: var(--space-16); } .card { position: relative; - border: 1px solid var(--border); - border-radius: 0.4rem; - padding: 0.75rem 1rem; - font-size: 0.92rem; -} -.card > p:first-child { margin-top: 0; } + padding: var(--space-12) var(--space-16); + border: 1px solid var(--line); + border-radius: var(--radius-card); + font-size: 0.875rem; + line-height: 1.5; + transition: background-color var(--vx-motion); +} +.card:hover { background: var(--wash); } +.vx-docs-main .card > * { margin: 0 0 var(--space-8); } +.vx-docs-main .card > :last-child { margin-bottom: 0; } +.vx-docs-main .card > p:first-child a { text-decoration: none; } .card > p:first-child a::after { content: ""; position: absolute; inset: 0; } -.card > :last-child { margin-bottom: 0; } - -#search-results .search { list-style: none; padding-left: 0; } -#search-results .search li { margin: 1rem 0; } -span.highlighted { background: var(--mark); } -.indextable { width: 100%; } -.indextable td { vertical-align: top; padding: 0.1rem 0.5rem; } - -blockquote { margin: 1rem 0; padding-left: 1rem; border-left: 3px solid var(--border); color: var(--fg-muted); } -hr { border: 0; border-top: 1px solid var(--border); margin: 2rem 0; } - -@media (max-width: 70em) { - .pagetoc { display: none; } -} -@media (max-width: 50em) { - .layout { display: block; } - .sidebar { - display: none; - position: static; - max-height: none; - border-right: 0; - border-bottom: 1px solid var(--border); - } - #nav-toggle:checked ~ .layout .sidebar { display: block; } - .nav-toggle { display: block; } - .search { max-width: none; } -} +/* The versioning spec's terms and diagrams. The diagrams draw their own light panels. */ +table.versioning-terms td:first-child { white-space: nowrap; } +figure.versioning-diagram { margin-inline: 0; } +figure.versioning-diagram img { border-radius: var(--radius-card); } diff --git a/docs/conf.py b/docs/conf.py index 8bddc847055..e404247d822 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,7 +36,6 @@ "vortex_site_files", ] -templates_path = ["_templates"] html_show_sourcelink = False exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "README.md", "AGENTS.md"] @@ -198,30 +197,6 @@ def _convert_python_fenced_blocks_from_rust_to_valid_reST_blocks( in_block = False -def _resolve_breathe_cpp_references(app, env, node, contnode) -> object | None: - """Resolve relative C++ references emitted by Breathe. - - Breathe emits cross-namespace parameter types with relative qualifiers (e.g. ``scalar::Scalar`` - instead of ``vortex::scalar::Scalar``). This handler intercepts unresolved references and - re-resolves them under the ``vortex::`` namespace. - """ - if node.get("refdomain") != "cpp" or node.get("reftype") != "identifier": - return None - - target = node.get("reftarget", "") - if not target or target.startswith("vortex::"): - return None - - cpp_domain = env.get_domain("cpp") - # Try resolving with the vortex:: prefix. - node = node.deepcopy() - node["reftarget"] = f"vortex::{target}" - return cpp_domain.resolve_xref( - env, node.get("refdoc", ""), app.builder, "identifier", node["reftarget"], node, contnode - ) - - def setup(app) -> None: app.connect("hawkmoth-process-docstring", _replace_rust_references) app.connect("autodoc-process-docstring", _convert_python_fenced_blocks_from_rust_to_valid_reST_blocks) - app.connect("missing-reference", _resolve_breathe_cpp_references) diff --git a/docs/pyproject.toml b/docs/pyproject.toml index 9501bdf3d42..fd2e6a872bf 100644 --- a/docs/pyproject.toml +++ b/docs/pyproject.toml @@ -4,7 +4,6 @@ version = "0.1.0" description = "Vortex documentation." authors = [] dependencies = [ - "accessible-pygments>=0.0.5", "hawkmoth>=0.20.0", "myst-parser>=4.0.0", "rcssmin>=1.1.2", diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md index a6272bd8a8e..acef159205c 100644 --- a/docs/specs/versioning/compatibility.md +++ b/docs/specs/versioning/compatibility.md @@ -40,6 +40,7 @@ plugins. It assumes correct implementations, edition checks enabled, and full de ```{figure} ../../_static/versioning-compatibility.svg :alt: Plugins select and validate wire formats while adapting arrays between memory and storage. :target: ../../_static/versioning-compatibility.svg +:figclass: versioning-diagram The writer selects a plugin by the array's in-memory ID, whereas the reader selects a plugin by the stored wire ID. In either direction, the plugin can adapt the array structure while preserving diff --git a/uv.lock b/uv.lock index e1b9619a0a5..e995fefc3dc 100644 --- a/uv.lock +++ b/uv.lock @@ -28,18 +28,6 @@ members = [ "workspace", ] -[[package]] -name = "accessible-pygments" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, -] - [[package]] name = "aiohappyeyeballs" version = "2.7.1" @@ -547,7 +535,6 @@ name = "docs" version = "0.1.0" source = { virtual = "docs" } dependencies = [ - { name = "accessible-pygments" }, { name = "hawkmoth" }, { name = "libclang" }, { name = "myst-parser" }, @@ -560,7 +547,6 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "accessible-pygments", specifier = ">=0.0.5" }, { name = "hawkmoth", specifier = ">=0.20.0" }, { name = "libclang", specifier = ">=18.1.1" }, { name = "myst-parser", specifier = ">=4.0.0" }, From b8c5932cf51f5df2f0dac85db228835f3ac37b66 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 4 Nov 2026 16:23:04 -0500 Subject: [PATCH 37/41] docs: require vortex_ensure_eq for equality checks (#11246) ## Summary Equality failures are easier to diagnose when errors include both values. Agent guidance should require `vortex_ensure_eq!` whenever possible so new equality checks include those diagnostics. ## Changes Added the rule to the Rust code style guidance in `AGENTS.md`. Signed-off-by: Connor Tsui --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 6ffe3a768f0..a6fad2b8839 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -191,6 +191,8 @@ These CI checks are the ones most often missed when adding files rather than edi per-element loops; back such changes with the benchmarks it names. - Only write comments that explain non-obvious logic or important context. Do not comment self-explanatory code. +- Use `vortex_ensure_eq!(left, right)` instead of `vortex_ensure!(left == right)` whenever + possible so equality failures report both values. ## Tests From afaabf377534974356c5fc1dcd43cce7528e8b75 Mon Sep 17 00:00:00 2001 From: Will Manning Date: Wed, 4 Nov 2026 17:45:45 -0500 Subject: [PATCH 38/41] Show the same header, footer, 404, and theme switch on the docs and Explorer (#11249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change gives the docs and the Explorer the same chrome as vortex.dev and bench, and serves both from one Vercel project. - The docs and the Explorer show the shared header, footer, and theme switch. The theme is Dark by default, also when the OS uses Light. The vx-theme cookie keeps the choice on all vortex.dev subdomains. - Both show one 404, with a 404 status. The docs build it, and it is the same as the 404 on vortex.dev. The Explorer has no 404 of its own now. The Explorer's start page shows the dot field. - The docs' Vercel project deploys the Explorer. A preview serves it at `/explorer/`. explore.vortex.dev serves it at the root, and docs.vortex.dev/explorer redirects there. After merge, move the explore.vortex.dev domain to the docs' Vercel project. Until then, it serves its last deploy. Not in this change: one favicon set, one share image, and the GitHub star count. ## Screenshots Before is production. After is a local build.
Docs Dark, 1440px ![Docs, dark, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs_dark_1440.png) Dark, 390px ![Docs, dark, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs_dark_390.png) Light, 1440px ![Docs, light, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs_light_1440.png) Light, 390px ![Docs, light, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs_light_390.png)
Docs footer Dark, 1440px ![Docs footer, dark, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs-footer_dark_1440.png) Light, 1440px ![Docs footer, light, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs-footer_light_1440.png)
Docs 404 Dark, 1440px ![Docs 404, dark, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs-404_dark_1440.png) Dark, 390px ![Docs 404, dark, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs-404_dark_390.png) Light, 1440px ![Docs 404, light, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs-404_light_1440.png) Light, 390px ![Docs 404, light, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/docs-404_light_390.png)
Explorer start page Dark, 1440px ![Explorer start page, dark, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer_dark_1440.png) Dark, 390px ![Explorer start page, dark, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer_dark_390.png) Light, 1440px ![Explorer start page, light, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer_light_1440.png) Light, 390px ![Explorer start page, light, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer_light_390.png)
Explorer file view Dark, 1440px ![Explorer file view, dark, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer-file_dark_1440.png) Dark, 390px ![Explorer file view, dark, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer-file_dark_390.png) Light, 1440px ![Explorer file view, light, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer-file_light_1440.png) Light, 390px ![Explorer file view, light, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer-file_light_390.png)
Explorer 404 Dark, 1440px ![Explorer 404, dark, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer-404_dark_1440.png) Dark, 390px ![Explorer 404, dark, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer-404_dark_390.png) Light, 1440px ![Explorer 404, light, 1440px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer-404_light_1440.png) Light, 390px ![Explorer 404, light, 390px](https://gist.githubusercontent.com/lwwmanning/7cc3642656c2dd2652226f109a967a0e/raw/explorer-404_light_390.png)
🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Will Manning --- .github/vercel-cli/pages-html-routes.py | 13 +- .github/workflows/docs-preview.yml | 18 +- .github/workflows/docs.yml | 11 +- .github/workflows/web-build.yml | 56 ++ .github/workflows/web.yml | 54 +- CONTRIBUTING.md | 8 +- LICENSES/LicenseRef-Third-Party.txt | 6 + REUSE.toml | 16 + _typos.toml | 7 +- docs/_ext/vortex_theme.py | 18 +- docs/_static/lf_white.svg | 42 ++ docs/_static/vortex-header.js | 21 + docs/_static/vortex-mark/ascii-core.js | 133 +++++ docs/_static/vortex-mark/ascii-mark.js | 348 +++++++++++ docs/_static/vortex-mark/three.module.min.js | 6 + .../vortex-mark/vortex-mark-geometry.js | 13 + docs/_static/vortex-status-mark.js | 35 ++ docs/_static/vortex-theme-bootstrap.js | 5 + docs/_static/vortex-theme.js | 164 ++++++ docs/_static/vortex_symbol_dark_theme.svg | 12 + docs/_theme/vortex/404.html | 30 + docs/_theme/vortex/layout.html | 96 +-- docs/_theme/vortex/system/vortex-tokens.css | 22 +- docs/_theme/vortex/system/vortex.css | 441 ++++++++++++-- docs/_theme/vortex/vortex.css | 23 +- docs/conf.py | 2 + docs/vercel-output-config.json | 95 ++- vortex-web/README.md | 4 + vortex-web/biome.jsonc | 13 +- vortex-web/index.html | 2 +- vortex-web/public/lf_white.svg | 42 ++ vortex-web/public/theme.js | 15 +- vortex-web/src/components/MarkGithub.tsx | 11 + .../components/explorer/FileDropScreen.tsx | 10 +- .../src/components/explorer/FileHeader.tsx | 82 +-- vortex-web/src/components/explorer/Footer.tsx | 51 ++ vortex-web/src/components/explorer/Header.tsx | 114 +++- vortex-web/src/contexts/ThemeContext.tsx | 49 +- vortex-web/src/styles/explorer.css | 22 +- vortex-web/src/styles/vortex-base.css | 2 +- vortex-web/src/styles/vortex-tokens.css | 9 +- vortex-web/src/styles/vortex.css | 548 ++++++++---------- .../src/vendor/vortex-design/theme.d.ts | 28 + vortex-web/src/vendor/vortex-design/theme.js | 164 ++++++ vortex-web/vercel-output-config.json | 42 -- vortex-web/vite.config.ts | 4 + 46 files changed, 2286 insertions(+), 621 deletions(-) create mode 100644 .github/workflows/web-build.yml create mode 100644 LICENSES/LicenseRef-Third-Party.txt create mode 100755 docs/_static/lf_white.svg create mode 100644 docs/_static/vortex-header.js create mode 100644 docs/_static/vortex-mark/ascii-core.js create mode 100644 docs/_static/vortex-mark/ascii-mark.js create mode 100644 docs/_static/vortex-mark/three.module.min.js create mode 100644 docs/_static/vortex-mark/vortex-mark-geometry.js create mode 100644 docs/_static/vortex-status-mark.js create mode 100644 docs/_static/vortex-theme-bootstrap.js create mode 100644 docs/_static/vortex-theme.js create mode 100644 docs/_static/vortex_symbol_dark_theme.svg create mode 100644 docs/_theme/vortex/404.html create mode 100755 vortex-web/public/lf_white.svg create mode 100644 vortex-web/src/components/MarkGithub.tsx create mode 100644 vortex-web/src/components/explorer/Footer.tsx create mode 100644 vortex-web/src/vendor/vortex-design/theme.d.ts create mode 100644 vortex-web/src/vendor/vortex-design/theme.js delete mode 100644 vortex-web/vercel-output-config.json diff --git a/.github/vercel-cli/pages-html-routes.py b/.github/vercel-cli/pages-html-routes.py index c4952910308..995af6c4c83 100644 --- a/.github/vercel-cli/pages-html-routes.py +++ b/.github/vercel-cli/pages-html-routes.py @@ -12,8 +12,9 @@ Usage: pages-html-routes.py OUTPUT_DIR OUTPUT_DIR is a `.vercel/output` directory holding `static/` and a -`config.json` whose routes only set headers. The routes this script adds -follow those header routes. +`config.json` whose routes set headers, or redirect or rewrite a request +that the HTML routes must not see, such as the Explorer's host. The routes +this script adds follow them. """ import json @@ -44,6 +45,12 @@ def html_routes(static: Path) -> list[dict]: # Only reached when no file or directory matched. redirect(r"^/(.+)/$", "/$1", 307), {"src": r"^/(.+)$", "dest": "/$1.html", "check": True}, + # An address that matches no file gets the build's 404 page, with a 404 status. + *( + [{"handle": "error"}, {"src": "^/.*$", "dest": "/404.html", "status": 404}] + if (static / "404.html").is_file() + else [] + ), ] @@ -52,7 +59,7 @@ def main(output: Path) -> None: config = json.loads(config_path.read_text()) routes = config.get("routes", []) if any("handle" in route for route in routes): - sys.exit(f"{config_path}: routes must only set headers when HTML handling is added") + sys.exit(f"{config_path}: routes must not start a phase ('handle') when HTML handling is added") config["routes"] = routes + html_routes(output / "static") config_path.write_text(json.dumps(config, indent=2) + "\n") diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index 480b20d16c9..a3dabc5e9fd 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -52,9 +52,10 @@ jobs: '.github/workflows/docs-build.yml', '.github/workflows/docs-preview.yml', '.github/workflows/deploy-vercel.yml', + '.github/workflows/web-build.yml', ]); eligible = files.some(file => [file.filename, file.previous_filename].some( - path => path && (path.startsWith('docs/') || + path => path && (path.startsWith('docs/') || path.startsWith('vortex-web/') || path.startsWith('.github/vercel-cli/') || workflows.has(path)), )); } @@ -71,11 +72,22 @@ jobs: cancel-in-progress: true uses: ./.github/workflows/docs-build.yml + web: + name: Build Explorer preview + needs: changes + if: needs.changes.outputs.eligible == 'true' + permissions: + contents: read + concurrency: + group: docs-preview-web-${{ github.event.pull_request.number }} + cancel-in-progress: true + uses: ./.github/workflows/web-build.yml + # Each Vercel deployment has its own URL, so a stale build cannot replace # a newer preview, and the PR needs no recheck before publishing. publish: name: Publish preview - needs: build + needs: [build, web] permissions: contents: read deployments: write @@ -85,6 +97,6 @@ jobs: secrets: inherit with: environment: vercel-docs - artifact-ids: ${{ needs.build.outputs.artifact-id }} + artifact-ids: ${{ needs.build.outputs.artifact-id }},${{ needs.web.outputs.artifact-id }} config: docs/vercel-output-config.json pages-html-handling: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 691b2fdd31e..5884f927b21 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,9 +21,16 @@ jobs: build: uses: ./.github/workflows/docs-build.yml + web: + name: Build Explorer + permissions: + contents: read + uses: ./.github/workflows/web-build.yml + + # One Vercel project serves the docs and the Explorer. deploy: name: Deploy to Vercel - needs: build + needs: [build, web] permissions: contents: read deployments: write @@ -33,7 +40,7 @@ jobs: secrets: inherit with: environment: vercel-docs - artifact-ids: ${{ needs.build.outputs.artifact-id }} + artifact-ids: ${{ needs.build.outputs.artifact-id }},${{ needs.web.outputs.artifact-id }} config: docs/vercel-output-config.json pages-html-handling: true production: ${{ github.ref == 'refs/heads/develop' }} diff --git a/.github/workflows/web-build.yml b/.github/workflows/web-build.yml new file mode 100644 index 00000000000..eff8842029c --- /dev/null +++ b/.github/workflows/web-build.yml @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + +name: Build Vortex Explorer + +# The docs' Vercel project serves the Explorer: under /explorer/ on the docs +# and their previews, and at the root of explore.vortex.dev (see +# docs/vercel-output-config.json). So the artifact holds an explorer/ +# directory, which the deploy merges into the docs build. +on: + workflow_call: + outputs: + artifact-id: + description: "Explorer artifact for this build" + value: ${{ jobs.build.outputs.artifact-id }} + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + outputs: + artifact-id: ${{ steps.artifact.outputs.artifact-id }} + defaults: + run: + working-directory: vortex-web + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - uses: ./.github/actions/setup-rust + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install wasm-pack + uses: taiki-e/install-action@1e29709c0ec63babbe8f266397a465f29a825f91 # v2 + with: + tool: wasm-pack@0.15.0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.4.3 + # Vite runs on Node, and Bun runs its scripts with it. + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 + with: + node-version: "24" + - run: bun install --frozen-lockfile + - run: bun run build + - name: Put the build under explorer/ + run: mkdir site && mv dist site/explorer + - name: Upload build + id: artifact + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: vortex-explorer-${{ github.run_attempt }} + path: vortex-web/site + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 65ddc32cb33..d08d04335aa 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -19,6 +19,8 @@ defaults: run: working-directory: vortex-web +# The docs workflows build and deploy the Explorer (web-build.yml), because +# one Vercel project serves both. jobs: changes: name: Detect Changes @@ -38,8 +40,6 @@ jobs: web: - "vortex-web/**" - ".github/workflows/web.yml" - - ".github/workflows/deploy-vercel.yml" - - ".github/vercel-cli/**" check: name: Check & Lint @@ -69,53 +69,3 @@ jobs: - run: bun run lint - run: bun run typecheck - run: bun run build-storybook - - build: - name: Build - needs: [changes] - if: needs.changes.outputs.web == 'true' - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: ./.github/actions/setup-rust - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install wasm-pack - uses: taiki-e/install-action@1e29709c0ec63babbe8f266397a465f29a825f91 # v2 - with: - tool: wasm-pack@0.15.0 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - bun-version: 1.4.3 - # Storybook and Vite run on Node, and Bun runs their scripts with it. - - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 - with: - node-version: "24" - - run: bun install --frozen-lockfile - - run: bun run build - - name: Upload build artifact - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 - with: - name: vortex-explorer - path: vortex-web/dist/ - - deploy: - name: Deploy to Vercel - needs: [build] - # Fork PRs get no secrets, so only pushes and same-repo PRs deploy. - if: >- - github.event_name == 'push' || - github.event.pull_request.head.repo.full_name == github.repository - permissions: - contents: read - deployments: write - uses: ./.github/workflows/deploy-vercel.yml - # A called workflow only sees the secrets its caller passes, including - # the secrets of the environment the called job deploys to. - secrets: inherit - with: - environment: vercel-explorer - artifact-name: vortex-explorer - config: vortex-web/vercel-output-config.json - production: ${{ github.event_name == 'push' }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21a2bc177bb..862798e1e3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -158,8 +158,9 @@ Use `make -C docs help` to list focused targets, and finish documentation change #### PR previews -PRs with branches in `vortex-data/vortex` get a docs preview when they change `docs/`, a docs -workflow, or the Vercel deploy files in `.github/vercel-cli/`. This includes draft PRs and PRs +PRs with branches in `vortex-data/vortex` get a docs preview when they change `docs/`, +`vortex-web/`, a docs or Explorer build workflow, or the Vercel deploy files in +`.github/vercel-cli/`. This includes draft PRs and PRs targeting another feature branch. Fork PRs do not get hosted previews. Resolve merge conflicts before building a preview. @@ -168,7 +169,8 @@ source code. The label stays on the PR and enables previews for later pushes. Re updates only if the PR no longer changes docs or a docs workflow. Previews are hosted on Vercel. The `vercel-docs` deployment status links to the latest preview for -the PR, and the workflow summary records its URL. Each preview keeps its own URL. Previews build the +the PR, and the workflow summary records its URL. One Vercel project serves the docs and the +Explorer, so each preview also serves the Explorer at `/explorer/`. Each preview keeps its own URL. Previews build the PR head commit. Eligibility uses the complete PR diff, so rebasing over upstream docs changes does not enable a preview for an unrelated PR. diff --git a/LICENSES/LicenseRef-Third-Party.txt b/LICENSES/LicenseRef-Third-Party.txt new file mode 100644 index 00000000000..e5f7ba5df2f --- /dev/null +++ b/LICENSES/LicenseRef-Third-Party.txt @@ -0,0 +1,6 @@ +Files marked LicenseRef-Third-Party are not licensed by the Vortex project. + +The Apache-2.0 and CC-BY-4.0 grants that cover the rest of this repository do not apply to +them. Copyright in each file belongs to the holder named in its SPDX-FileCopyrightText entry +in REUSE.toml, and any trademarks it shows belong to their owners. For permission to reuse +one of these files, contact its holder. diff --git a/REUSE.toml b/REUSE.toml index b371aaf2d67..b8cf3f82644 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -21,6 +21,22 @@ precedence = "override" SPDX-FileCopyrightText = "2024 The Geist Project Authors (https://github.com/vercel/geist-font.git)" SPDX-License-Identifier = "OFL-1.1" +# The Linux Foundation's logo, in the footer of the docs and the Explorer, belongs to the Linux +# Foundation. +[[annotations]] +path = ["docs/_static/lf_white.svg", "vortex-web/public/lf_white.svg"] +precedence = "override" +SPDX-FileCopyrightText = "The Linux Foundation" +SPDX-License-Identifier = "LicenseRef-Third-Party" + +# three.js, which the docs serve for the 404's mark, because their CSP allows scripts from their +# own origin only. A copy of three@0.170.0's build/three.module.min.js. +[[annotations]] +path = "docs/_static/vortex-mark/three.module.min.js" +precedence = "override" +SPDX-FileCopyrightText = "2010-2024 Three.js Authors" +SPDX-License-Identifier = "MIT" + # BtrBlocks diagram is licensed under CC-BY-4.0. [[annotations]] path = "vortex-btrblocks/decision-tree.*" diff --git a/_typos.toml b/_typos.toml index 6b7a9a80e90..230bcced0c8 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,5 +1,6 @@ [default] -extend-ignore-identifiers-re = ["ffor", "FFOR", "FoR", "typ", "ratatui", "wht", "WHT"] +# metalness is a three.js material property, which the ASCII mark sets. +extend-ignore-identifiers-re = ["ffor", "FFOR", "FoR", "typ", "ratatui", "wht", "WHT", "metalness"] # We support a few common special comments to tell the checker to ignore sections of code extend-ignore-re = [ "(#|//)\\s*spellchecker:ignore-next-line\\n.*", # Ignore the next line @@ -14,7 +15,9 @@ extend-ignore-re = [ extend-exclude = ["/vortex-bench/**", "/docs/references.bib", "benchmarks/**", "vortex-sqllogictest/slt/**", "encodings/fsst/src/dfa/tests.rs", "encodings/fsst/src/dfa/flat_contains.rs", - "scripts/measurement_id_golden.json", "scripts/ml64_reproducible.py"] + "scripts/measurement_id_golden.json", "scripts/ml64_reproducible.py", + # three.js, minified, which the docs serve for the 404's mark. + "docs/_static/vortex-mark/three.module.min.js"] [type.py] extend-ignore-identifiers-re = [ diff --git a/docs/_ext/vortex_theme.py b/docs/_ext/vortex_theme.py index 0694c4977ba..721e74e8616 100644 --- a/docs/_ext/vortex_theme.py +++ b/docs/_ext/vortex_theme.py @@ -4,7 +4,8 @@ and fail the build if a copy of the system was edited by hand 2. Load only one CSS and no JS per page except search page 3. Rewrite TOC into detail/summary with regex so it loads without JS -4. Minify CSS +4. Make the 404 page's relative URLs root-relative, since it is served at any address +5. Minify CSS """ from __future__ import annotations @@ -102,9 +103,24 @@ def _toc(app: Sphinx, pagename: str, templatename: str, context: dict[str, Any], context["vortex_globaltoc"] = html +# The 404 page is served at whatever address missed, so its relative links and assets would resolve +# against that address. Every relative URL in it becomes root-relative: links, assets, and the +# header's search form. +_RELATIVE_URL = re.compile(r'((?:href|src|action)=")(?![a-z][a-z0-9+.-]*:|/|#)') + + +def _root_relative_404(app: Sphinx, exception: Exception | None) -> None: + if exception is not None or app.builder.format != "html": + return + page = Path(app.outdir) / "404.html" + if page.exists(): + page.write_text(_RELATIVE_URL.sub(r"\1/", page.read_text())) + + def setup(app: Sphinx) -> dict[str, Any]: _GEN_STATIC.mkdir(parents=True, exist_ok=True) app.connect("builder-inited", _css) app.connect("html-page-context", _filter_assets) app.connect("html-page-context", _toc) + app.connect("build-finished", _root_relative_404) return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_static/lf_white.svg b/docs/_static/lf_white.svg new file mode 100755 index 00000000000..316c64c84c4 --- /dev/null +++ b/docs/_static/lf_white.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/vortex-header.js b/docs/_static/vortex-header.js new file mode 100644 index 00000000000..dced92c1c53 --- /dev/null +++ b/docs/_static/vortex-header.js @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +// The shared header's scripts (the design system's Header and ThemeSwitch): every theme switch on +// the page, and the Menu, which closes on Escape (focus goes back to its button) and on a click +// outside it. Without scripts the Menu still opens and closes, and the theme follows the OS. +import { mountThemeSwitch } from "./vortex-theme.js"; + +for (const el of document.querySelectorAll(".vx-theme-switch")) mountThemeSwitch(el); + +const menu = document.querySelector(".vx-header-menu"); +if (menu) { + document.addEventListener("keydown", (e) => { + if (e.key !== "Escape" || !menu.open) return; + menu.open = false; + menu.querySelector("summary")?.focus(); + }); + document.addEventListener("pointerdown", (e) => { + if (menu.open && !menu.contains(e.target)) menu.open = false; + }); +} diff --git a/docs/_static/vortex-mark/ascii-core.js b/docs/_static/vortex-mark/ascii-core.js new file mode 100644 index 00000000000..b0d6d5b1f42 --- /dev/null +++ b/docs/_static/vortex-mark/ascii-core.js @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +// A copy of the Vortex design system's components/lib/ascii-core.js. Edit the system, then copy it again. + +// The rules of the ASCII mark and the logo banner, kept free of the DOM so they can be tested. + +/** Characters from empty to dense, mapped from luminance 0..1. */ +export const RAMP = " .:-=+*#%@"; +const HEX = "0123456789abcdef"; + +/** The ramp character for luminance `v`, clamped to 0..1. */ +export function rampChar(v) { + const x = Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0)); + return RAMP[Math.round(x * (RAMP.length - 1))]; +} + +/** 1 inside the solid core, 0 at and beyond `reach`, linear between. */ +export function falloff(d, reach, core = 0.3) { + const c = reach * core; + if (d <= c) return 1; + if (d >= reach) return 0; + return 1 - (d - c) / (reach - c); +} + +/** The strongest brush mark at cell centre (cx, cy). Each mark is {x, y, reach, k}. */ +export function brushStrength(cx, cy, marks) { + let s = 0; + for (const m of marks) s = Math.max(s, falloff(Math.hypot(cx - m.x, cy - m.y), m.reach) * m.k); + return s; +} + +/** A hex digit for a cell that flickers with `tick`. Always one of 0-9a-f, for any integers. */ +export function hexDigit(row, col, tick) { + let h = Math.imul((row | 0) * 4096 + (col | 0), 2654435761) >>> 0; + h = (h ^ (h >>> 15)) >>> 0; + const speed = (h >>> 8) % 3 + 1; + return HEX[(((h + Math.imul(tick | 0, speed)) >>> 0) % 16)]; +} + +/** How many copies of a logo set the banner needs so that two loops cover the view, always even. */ +export function copiesNeeded(setWidth, viewWidth) { + const n = Math.max(2, Math.ceil((2 * viewWidth) / Math.max(1, setWidth))); + return n % 2 ? n + 1 : n; +} + +/** + * The camera's vertical field of view (degrees) for a host of `aspect` (width / height). The mark + * is framed for a square: a wide host keeps `base` and shows more around it, and a tall host opens + * the vertical field until the horizontal one equals `base`, so the mark's sides are never cropped. + */ +export function fitFov(base, aspect) { + if (aspect >= 1) return base; + return (2 * Math.atan(Math.tan((base * Math.PI) / 360) / aspect) * 180) / Math.PI; +} + +/** + * The tokens the mark draws with in a colour scheme ("dark" or "light"; anything else is Dark): + * four inks that step away from the ground as the lit faces brighten, the field's dot, and the + * brush's outer and inner colours. Light mirrors Dark's steps of contrast against its own ground. + */ +export function markPalette(scheme) { + const light = scheme === "light"; + return { + inks: light ? ["--grey-400", "--grey-450", "--grey-600", "--black"] : ["--grey-600", "--grey-500", "--grey-400", "--grey-50"], + field: light ? "--grey-300" : "--grey-700", + // The trail's edge, then its lighter core. Light's accent and accent-ink are one Blue, so the + // brush names the scale to keep two tones in both themes. + brush: light ? ["--blue", "--blue-bright"] : ["--blue-bright", "--blue-soft"], + }; +} + +/** + * Reads the mark's render back for the character grid without stalling the GPU every frame. + * `render()` draws the scene into the render target. `readSync(buf)` reads it into `buf` at once, + * so the page waits for the GPU. `readAsync(buf)`, when the renderer has one (WebGL2's pixel pack + * buffer and fence), starts a read and resolves once `buf` holds it. Call `resize(bytes)` whenever + * the target changes size, then `next()` once a frame: it returns the newest complete pixels. + * The first frame after a resize reads at once, so the mark never shows an empty field. After it, + * one read is in flight at a time and the scene renders only when a read starts, so the grid lags + * the model by a frame or so. A failed async read falls back to reading at once. + */ +export function frameReader({ render, readSync, readAsync = null }) { + let front = new Uint8Array(0); + let back = front; + let gen = 0; + let busy = false; + let stale = true; + let read = readAsync; + return { + resize(bytes) { + front = new Uint8Array(bytes); + back = new Uint8Array(bytes); + gen++; + busy = false; + stale = true; + }, + next() { + if (stale || !read) { + render(); + readSync(front); + stale = false; + return front; + } + if (!busy) { + const g = gen; + const buf = back; + busy = true; + render(); + // The read starts now, right after the render, and lands on a later frame. + let landed; + try { + landed = Promise.resolve(read(buf)); + } catch (err) { + landed = Promise.reject(err); + } + landed.then( + () => { + if (g !== gen) return; + back = front; + front = buf; + busy = false; + }, + () => { + if (g !== gen) return; + read = null; + busy = false; + }, + ); + } + return front; + }, + }; +} diff --git a/docs/_static/vortex-mark/ascii-mark.js b/docs/_static/vortex-mark/ascii-mark.js new file mode 100644 index 00000000000..da7f765e25b --- /dev/null +++ b/docs/_static/vortex-mark/ascii-mark.js @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +// A copy of the Vortex design system's components/lib/ascii-mark.js. Edit the system, then copy it again. + +// The Vortex mark, turning in 3D and redrawn every frame as a grid of Geist Mono characters. +// The pointer leaves a short Blue trail in which the characters become flickering hex bytes. +// Under reduced motion it draws one still frame and ignores the pointer. Without WebGL, or when +// three.js cannot load, it shows the static symbol. It draws in the host's colour scheme and +// follows a change of data-theme on the page. +import { brushStrength, fitFov, frameReader, hexDigit, markPalette, rampChar } from "./ascii-core.js"; +import { decode } from "./vortex-mark-geometry.js"; + +const SCALE = 3.0; +const TILT = -0.3; +const YAW = -0.55; +const THICK = 1.6; +const OFFSET = [-0.13, 0.04, 0]; +const SPIN = 14; // degrees per second +const LOOK = 0.22; +const LOOK_EASE = 0.06; +const TRAIL_MS = 450; +const TRAIL_STEP = 12; +const TRAIL = 8; +const SUB = 2; // render-target pixels per cell side +const THREE_URL = "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js"; + +/** + * Mounts the mark in `host` (positioned, sized by the caller). + * Options: patch (brush reach in px, 31), lean (follow the pointer, false), size and lineHeight + * of the character grid (12, 14), font (the grid's family; the host's --font-mono token by + * default), fallbackSrc (the symbol image for the static mode), mode ("auto", or "static" for the + * still symbol whatever the browser can do), three (a three.js module the site bundles, for pages + * whose CSP blocks the CDN), threeUrl (where to load three.js otherwise), exposure (how bright the + * lit faces read on the ramp, 2.5), field (draw the dot field around the mark, true; false when + * a DotField already lies under the host, so the page shows one grid of dots, not two). The mark + * is decorative: keep the host aria-hidden. + * Colours follow the host's colour-scheme, which the tokens set per theme: Dark greys on a Dark + * ground, mirrored greys in Light (markPalette in ascii-core.js), and a two-tone Blue brush. In + * Light the symbol image is inverted, so pass the white one in both themes. + * Resolves to { mode: "live" | "reduced" | "static", destroy() }. Call destroy() when the host + * leaves the page: it stops the loop, removes the listeners and frees the WebGL context. + */ +export async function mountAsciiMark(host, opts = {}) { + const cleanups = []; + const destroy = () => { + while (cleanups.length) cleanups.pop()(); + }; + // A step that throws part-way (a lost WebGL context, a bad three.js) must not leave the observer, + // the canvas, or the context alive: the caller never gets destroy() from a rejected promise. + try { + return await mount(host, opts, cleanups, destroy); + } catch (err) { + destroy(); + throw err; + } +} + +async function mount(host, opts, cleanups, destroy) { + const hostCss = getComputedStyle(host); + const o = { + font: hostCss.getPropertyValue("--font-mono").trim() || '"Geist Mono", ui-monospace, monospace', + mode: "auto", + size: 12, + lineHeight: 14, + patch: 31, + lean: false, + fallbackSrc: null, + three: null, + threeUrl: THREE_URL, + exposure: 2.5, + field: true, + ...opts, + }; + // The palette for the host's scheme, read again whenever a data-theme changes on the page. + let inks, field, brush, light; + const paint = () => { + const css = getComputedStyle(host); + light = css.colorScheme === "light"; + const p = markPalette(light ? "light" : "dark"); + const read = (name, fallback) => css.getPropertyValue(name).trim() || fallback; + inks = p.inks.map((n) => read(n, "#888")); + field = read(p.field, light ? "#d4d4d2" : "#2b2b29"); + brush = light ? [read(p.brush[0], "#1d0fed"), read(p.brush[1], "#2f63ff")] : [read(p.brush[0], "#2f63ff"), read(p.brush[1], "#8fa9ff")]; + }; + paint(); + const repaints = []; + const themes = new MutationObserver(() => { + paint(); + for (const f of repaints) f(); + }); + themes.observe(document.documentElement, { attributes: true, subtree: true, attributeFilter: ["data-theme"] }); + cleanups.push(() => themes.disconnect()); + const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches; + + const still = () => { + if (o.fallbackSrc) { + const img = document.createElement("img"); + img.src = o.fallbackSrc; + img.alt = ""; + img.style.cssText = "position:absolute;inset:0;margin:auto;width:40%;height:auto;opacity:0.85"; + const tone = () => (img.style.filter = light ? "invert(1)" : ""); + tone(); + repaints.push(tone); + host.appendChild(img); + cleanups.push(() => img.remove()); + } + return { mode: "static", destroy }; + }; + + if (o.mode === "static") return still(); + let THREE; + let renderer; + try { + // A bundler must not try to resolve the CDN URL: sites under a CSP pass `three` instead. + THREE = o.three || (await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ /* @vite-ignore */ o.threeUrl)); + renderer = new THREE.WebGLRenderer({ antialias: false }); + } catch { + renderer = null; + } + if (!renderer) return still(); + cleanups.push(() => { + renderer.dispose(); + renderer.forceContextLoss(); + }); + + // The grid's font is a nicety: if it fails to download, draw with the fallback rather than not at all. + await document.fonts.load(`${o.size}px ${o.font}`).catch(() => {}); + const cv = document.createElement("canvas"); + cv.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block"; + // The dot field fades out toward the host's edges instead of ending in a hard rectangle. + cv.style.maskImage = cv.style.webkitMaskImage = "radial-gradient(closest-side, #000 78%, transparent 100%)"; + cv.setAttribute("aria-hidden", "true"); + host.appendChild(cv); + cleanups.push(() => cv.remove()); + const ctx = cv.getContext("2d"); + + const scene = new THREE.Scene(); + scene.background = new THREE.Color("#000000"); + scene.add(new THREE.AmbientLight(0xffffff, 0.08)); + const key = new THREE.DirectionalLight(0xffffff, 0.85); + key.position.set(1, 2, 6); + scene.add(key); + const fill = new THREE.DirectionalLight(0xffffff, 0.4); + fill.position.set(-3, -1, 1); + scene.add(fill); + const camera = new THREE.PerspectiveCamera(30, 1, 0.1, 100); + camera.position.set(0, 0, 6); + + const g = decode(); + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.BufferAttribute(g.positions, 3)); + if (g.normals) geometry.setAttribute("normal", new THREE.BufferAttribute(g.normals, 3)); + else geometry.computeVertexNormals(); + geometry.setIndex(new THREE.BufferAttribute(g.indices, 1)); + const material = new THREE.MeshStandardMaterial({ color: "#ffffff", metalness: 0, roughness: 0.6, flatShading: true }); + const mesh = new THREE.Mesh(geometry, material); + // The render target, made at the first resize. Declared before the cleanup that frees it. + let target = null; + cleanups.push(() => { + geometry.dispose(); + material.dispose(); + target?.dispose(); + }); + mesh.scale.set(1, THICK, 1); + const lean = new THREE.Group(); + lean.position.set(...OFFSET); + lean.rotation.y = YAW; + lean.scale.setScalar(SCALE); + const stand = new THREE.Group(); + stand.rotation.x = -Math.PI / 2 + TILT; + const spin = new THREE.Group(); + spin.add(mesh); + stand.add(spin); + lean.add(stand); + scene.add(lean); + + let cols = 0; + let rows = 0; + let cw = 0; + let W = 0; + let H = 0; + // The grid reads the render back from the GPU. Where three.js has an async read (WebGL2's pixel + // pack buffer and a fence), the page never waits for the GPU after the first frame: the grid + // draws from the newest read that has landed. Elsewhere, and for the still frame under reduced + // motion, it reads at once. three.js leaves its pack buffer bound while a read is in flight, + // which would make a later synchronous read fail, so it is unbound at once. + const gl = renderer.getContext(); + const canReadAsync = + !reduced && typeof renderer.readRenderTargetPixelsAsync === "function" && typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext; + const reader = frameReader({ + render: () => { + renderer.setRenderTarget(target); + renderer.render(scene, camera); + }, + readSync: (buf) => { + renderer.readRenderTargetPixels(target, 0, 0, cols * SUB, rows * SUB, buf); + renderer.setRenderTarget(null); + }, + readAsync: canReadAsync + ? (buf) => { + const landed = renderer.readRenderTargetPixelsAsync(target, 0, 0, cols * SUB, rows * SUB, buf); + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); + renderer.setRenderTarget(null); + return landed; + } + : null, + }); + // Lays the grid out for the host's size. A call at the same size changes nothing, so the + // ResizeObserver's first callback costs no second synchronous read. Returns whether it changed. + const layout = () => { + if (target && host.clientWidth === W && host.clientHeight === H) return false; + W = host.clientWidth; + H = host.clientHeight; + const dpr = Math.min(2, devicePixelRatio || 1); + cv.width = Math.round(W * dpr); + cv.height = Math.round(H * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.font = `${o.size}px ${o.font}`; + ctx.textBaseline = "top"; + cw = ctx.measureText("M").width; + cols = Math.max(1, Math.floor(W / cw)); + rows = Math.max(1, Math.floor(H / o.lineHeight)); + target?.dispose(); + target = new THREE.WebGLRenderTarget(cols * SUB, rows * SUB); + reader.resize(cols * SUB * rows * SUB * 4); + camera.aspect = (cols * cw) / (rows * o.lineHeight); + camera.fov = fitFov(30, camera.aspect); + camera.updateProjectionMatrix(); + return true; + }; + layout(); + + let look = null; + let head = null; + let trail = []; + const toLocal = (e) => { + const r = host.getBoundingClientRect(); + return { x: (e.clientX - r.left) * (W / r.width), y: (e.clientY - r.top) * (H / r.height) }; + }; + const onMove = (e) => { + const p = toLocal(e); + look = { x: (p.x / W) * 2 - 1, y: (p.y / H) * 2 - 1 }; + const last = trail.at(-1); + if (!last || Math.hypot(p.x - last.x, p.y - last.y) >= TRAIL_STEP) trail = [...trail, { ...p, born: performance.now() }].slice(-TRAIL); + head = { ...p, born: performance.now() }; + }; + const onLeave = () => { + look = null; + if (head) trail = [...trail, head].slice(-TRAIL); + head = null; + }; + + const draw = (now) => { + const pixels = reader.next(); + trail = trail.filter((s) => now - s.born < TRAIL_MS); + const marks = [ + ...trail.map((s) => { + const life = Math.max(0, 1 - (now - s.born) / TRAIL_MS); + return { x: s.x, y: s.y, reach: o.patch * (0.6 + life * 0.4), k: life * 0.85 }; + }), + ...(head ? [{ x: head.x, y: head.y, reach: o.patch, k: 1 }] : []), + ]; + ctx.clearRect(0, 0, W, H); + const ox = (W - cols * cw) / 2; + const oy = (H - rows * o.lineHeight) / 2; + const tick = Math.floor(now / 90); + const tw = cols * SUB; + const buckets = new Map(); + const put = (color, ch, x, y) => { + let b = buckets.get(color); + if (!b) buckets.set(color, (b = [])); + b.push(ch, x, y); + }; + for (let r = 0; r < rows; r++) + for (let c = 0; c < cols; c++) { + let s = 0; + for (let y = 0; y < SUB; y++) + for (let x = 0; x < SUB; x++) { + const i = (((rows - 1 - r) * SUB + y) * tw + c * SUB + x) * 4; + s += pixels[i] * 0.299 + pixels[i + 1] * 0.587 + pixels[i + 2] * 0.114; + } + const v = Math.min(1, ((s / (SUB * SUB * 255)) * o.exposure) ** 0.8); + const x = ox + c * cw; + const y = oy + r * o.lineHeight; + const b = marks.length && v > 0.03 ? brushStrength(x + cw / 2, y + o.lineHeight / 2, marks) : 0; + if (b > 0.12) put(b > 0.55 ? brush[1] : brush[0], hexDigit(r, c, tick), x, y); + else if (v > 0.03) put(inks[Math.min(inks.length - 1, Math.floor(v * inks.length))], rampChar(Math.max(v, 0.12)), x, y); + else if (o.field && r % 2 === 0 && c % 2 === 0) put(field, ".", x, y); + } + for (const [color, list] of buckets) { + ctx.fillStyle = color; + for (let i = 0; i < list.length; i += 3) ctx.fillText(list[i], list[i + 1], list[i + 2]); + } + }; + + // Reduced motion: one still frame, redrawn only when the host is resized. No brush. + if (reduced) { + draw(performance.now()); + repaints.push(() => draw(performance.now())); + const ro = new ResizeObserver(() => { + if (layout()) draw(performance.now()); + }); + ro.observe(host); + cleanups.push(() => ro.disconnect()); + return { mode: "reduced", destroy }; + } + + host.addEventListener("pointermove", onMove); + host.addEventListener("pointerleave", onLeave); + cleanups.push(() => { + host.removeEventListener("pointermove", onMove); + host.removeEventListener("pointerleave", onLeave); + }); + const ro = new ResizeObserver(layout); + ro.observe(host); + cleanups.push(() => ro.disconnect()); + + // The loop runs only while the host is on screen. + let raf = 0; + let last = performance.now(); + const frame = (now) => { + const dt = Math.min(0.05, (now - last) / 1000); + last = now; + spin.rotation.y += (SPIN * Math.PI * dt) / 180; + const at = o.lean ? look : null; + lean.rotation.y += (YAW + (at ? at.x * LOOK : 0) - lean.rotation.y) * LOOK_EASE; + lean.rotation.x += ((at ? -at.y * LOOK : 0) - lean.rotation.x) * LOOK_EASE; + draw(now); + raf = requestAnimationFrame(frame); + }; + const start = () => { + if (raf) return; + last = performance.now(); + raf = requestAnimationFrame(frame); + }; + const stop = () => { + cancelAnimationFrame(raf); + raf = 0; + }; + const io = new IntersectionObserver(([entry]) => (entry.isIntersecting ? start() : stop())); + io.observe(host); + cleanups.push(() => { + io.disconnect(); + stop(); + }); + start(); + return { mode: "live", destroy }; +} diff --git a/docs/_static/vortex-mark/three.module.min.js b/docs/_static/vortex-mark/three.module.min.js new file mode 100644 index 00000000000..282d1087068 --- /dev/null +++ b/docs/_static/vortex-mark/three.module.min.js @@ -0,0 +1,6 @@ +/** + * @license + * Copyright 2010-2024 Three.js Authors + * SPDX-License-Identifier: MIT + */ +const t="170",e={LEFT:0,MIDDLE:1,RIGHT:2,ROTATE:0,DOLLY:1,PAN:2},n={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},i=0,r=1,s=2,a=3,o=0,l=1,c=2,h=3,u=0,d=1,p=2,m=0,f=1,g=2,v=3,_=4,x=5,y=100,M=101,S=102,b=103,T=104,w=200,E=201,A=202,R=203,C=204,I=205,P=206,L=207,U=208,D=209,N=210,O=211,F=212,B=213,z=214,k=0,V=1,H=2,G=3,W=4,X=5,j=6,q=7,Y=0,Z=1,J=2,K=0,$=1,Q=2,tt=3,et=4,nt=5,it=6,rt=7,st="attached",at="detached",ot=300,lt=301,ct=302,ht=303,ut=304,dt=306,pt=1e3,mt=1001,ft=1002,gt=1003,vt=1004,_t=1004,xt=1005,yt=1005,Mt=1006,St=1007,bt=1007,Tt=1008,wt=1008,Et=1009,At=1010,Rt=1011,Ct=1012,It=1013,Pt=1014,Lt=1015,Ut=1016,Dt=1017,Nt=1018,Ot=1020,Ft=35902,Bt=1021,zt=1022,kt=1023,Vt=1024,Ht=1025,Gt=1026,Wt=1027,Xt=1028,jt=1029,qt=1030,Yt=1031,Zt=1032,Jt=1033,Kt=33776,$t=33777,Qt=33778,te=33779,ee=35840,ne=35841,ie=35842,re=35843,se=36196,ae=37492,oe=37496,le=37808,ce=37809,he=37810,ue=37811,de=37812,pe=37813,me=37814,fe=37815,ge=37816,ve=37817,_e=37818,xe=37819,ye=37820,Me=37821,Se=36492,be=36494,Te=36495,we=36283,Ee=36284,Ae=36285,Re=36286,Ce=2200,Ie=2201,Pe=2202,Le=2300,Ue=2301,De=2302,Ne=2400,Oe=2401,Fe=2402,Be=2500,ze=2501,ke=0,Ve=1,He=2,Ge=3200,We=3201,Xe=3202,je=3203,qe=0,Ye=1,Ze="",Je="srgb",Ke="srgb-linear",$e="linear",Qe="srgb",tn=0,en=7680,nn=7681,rn=7682,sn=7683,an=34055,on=34056,ln=5386,cn=512,hn=513,un=514,dn=515,pn=516,mn=517,fn=518,gn=519,vn=512,_n=513,xn=514,yn=515,Mn=516,Sn=517,bn=518,Tn=519,wn=35044,En=35048,An=35040,Rn=35045,Cn=35049,In=35041,Pn=35046,Ln=35050,Un=35042,Dn="100",Nn="300 es",On=2e3,Fn=2001;class Bn{addEventListener(t,e){void 0===this._listeners&&(this._listeners={});const n=this._listeners;void 0===n[t]&&(n[t]=[]),-1===n[t].indexOf(e)&&n[t].push(e)}hasEventListener(t,e){if(void 0===this._listeners)return!1;const n=this._listeners;return void 0!==n[t]&&-1!==n[t].indexOf(e)}removeEventListener(t,e){if(void 0===this._listeners)return;const n=this._listeners[t];if(void 0!==n){const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}dispatchEvent(t){if(void 0===this._listeners)return;const e=this._listeners[t.type];if(void 0!==e){t.target=this;const n=e.slice(0);for(let e=0,i=n.length;e>8&255]+zn[t>>16&255]+zn[t>>24&255]+"-"+zn[255&e]+zn[e>>8&255]+"-"+zn[e>>16&15|64]+zn[e>>24&255]+"-"+zn[63&n|128]+zn[n>>8&255]+"-"+zn[n>>16&255]+zn[n>>24&255]+zn[255&i]+zn[i>>8&255]+zn[i>>16&255]+zn[i>>24&255]).toLowerCase()}function Wn(t,e,n){return Math.max(e,Math.min(n,t))}function Xn(t,e){return(t%e+e)%e}function jn(t,e,n){return(1-n)*t+n*e}function qn(t,e){switch(e.constructor){case Float32Array:return t;case Uint32Array:return t/4294967295;case Uint16Array:return t/65535;case Uint8Array:return t/255;case Int32Array:return Math.max(t/2147483647,-1);case Int16Array:return Math.max(t/32767,-1);case Int8Array:return Math.max(t/127,-1);default:throw new Error("Invalid component type.")}}function Yn(t,e){switch(e.constructor){case Float32Array:return t;case Uint32Array:return Math.round(4294967295*t);case Uint16Array:return Math.round(65535*t);case Uint8Array:return Math.round(255*t);case Int32Array:return Math.round(2147483647*t);case Int16Array:return Math.round(32767*t);case Int8Array:return Math.round(127*t);default:throw new Error("Invalid component type.")}}const Zn={DEG2RAD:Vn,RAD2DEG:Hn,generateUUID:Gn,clamp:Wn,euclideanModulo:Xn,mapLinear:function(t,e,n,i,r){return i+(t-e)*(r-i)/(n-e)},inverseLerp:function(t,e,n){return t!==e?(n-t)/(e-t):0},lerp:jn,damp:function(t,e,n,i){return jn(t,e,1-Math.exp(-n*i))},pingpong:function(t,e=1){return e-Math.abs(Xn(t,2*e)-e)},smoothstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*(3-2*t)},smootherstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*t*(t*(6*t-15)+10)},randInt:function(t,e){return t+Math.floor(Math.random()*(e-t+1))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatSpread:function(t){return t*(.5-Math.random())},seededRandom:function(t){void 0!==t&&(kn=t);let e=kn+=1831565813;return e=Math.imul(e^e>>>15,1|e),e^=e+Math.imul(e^e>>>7,61|e),((e^e>>>14)>>>0)/4294967296},degToRad:function(t){return t*Vn},radToDeg:function(t){return t*Hn},isPowerOfTwo:function(t){return!(t&t-1)&&0!==t},ceilPowerOfTwo:function(t){return Math.pow(2,Math.ceil(Math.log(t)/Math.LN2))},floorPowerOfTwo:function(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))},setQuaternionFromProperEuler:function(t,e,n,i,r){const s=Math.cos,a=Math.sin,o=s(n/2),l=a(n/2),c=s((e+i)/2),h=a((e+i)/2),u=s((e-i)/2),d=a((e-i)/2),p=s((i-e)/2),m=a((i-e)/2);switch(r){case"XYX":t.set(o*h,l*u,l*d,o*c);break;case"YZY":t.set(l*d,o*h,l*u,o*c);break;case"ZXZ":t.set(l*u,l*d,o*h,o*c);break;case"XZX":t.set(o*h,l*m,l*p,o*c);break;case"YXY":t.set(l*p,o*h,l*m,o*c);break;case"ZYZ":t.set(l*m,l*p,o*h,o*c);break;default:console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+r)}},normalize:Yn,denormalize:qn};class Jn{constructor(t=0,e=0){Jn.prototype.isVector2=!0,this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t){return this.x+=t.x,this.y+=t.y,this}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=this.x,n=this.y,i=t.elements;return this.x=i[0]*e+i[3]*n+i[6],this.y=i[1]*e+i[4]*n+i[7],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this}clamp(t,e){return this.x=Math.max(t.x,Math.min(e.x,this.x)),this.y=Math.max(t.y,Math.min(e.y,this.y)),this}clampScalar(t,e){return this.x=Math.max(t,Math.min(e,this.x)),this.y=Math.max(t,Math.min(e,this.y)),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Math.max(t,Math.min(e,n)))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(0===e)return Math.PI/2;const n=this.dot(t)/e;return Math.acos(Wn(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y;return e*e+n*n}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this}equals(t){return t.x===this.x&&t.y===this.y}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this}rotateAround(t,e){const n=Math.cos(e),i=Math.sin(e),r=this.x-t.x,s=this.y-t.y;return this.x=r*n-s*i+t.x,this.y=r*i+s*n+t.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}}class Kn{constructor(t,e,n,i,r,s,a,o,l){Kn.prototype.isMatrix3=!0,this.elements=[1,0,0,0,1,0,0,0,1],void 0!==t&&this.set(t,e,n,i,r,s,a,o,l)}set(t,e,n,i,r,s,a,o,l){const c=this.elements;return c[0]=t,c[1]=i,c[2]=a,c[3]=e,c[4]=r,c[5]=o,c[6]=n,c[7]=s,c[8]=l,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],this}extractBasis(t,e,n){return t.setFromMatrix3Column(this,0),e.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(t){const e=t.elements;return this.set(e[0],e[4],e[8],e[1],e[5],e[9],e[2],e[6],e[10]),this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,i=e.elements,r=this.elements,s=n[0],a=n[3],o=n[6],l=n[1],c=n[4],h=n[7],u=n[2],d=n[5],p=n[8],m=i[0],f=i[3],g=i[6],v=i[1],_=i[4],x=i[7],y=i[2],M=i[5],S=i[8];return r[0]=s*m+a*v+o*y,r[3]=s*f+a*_+o*M,r[6]=s*g+a*x+o*S,r[1]=l*m+c*v+h*y,r[4]=l*f+c*_+h*M,r[7]=l*g+c*x+h*S,r[2]=u*m+d*v+p*y,r[5]=u*f+d*_+p*M,r[8]=u*g+d*x+p*S,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[3]*=t,e[6]*=t,e[1]*=t,e[4]*=t,e[7]*=t,e[2]*=t,e[5]*=t,e[8]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[1],i=t[2],r=t[3],s=t[4],a=t[5],o=t[6],l=t[7],c=t[8];return e*s*c-e*a*l-n*r*c+n*a*o+i*r*l-i*s*o}invert(){const t=this.elements,e=t[0],n=t[1],i=t[2],r=t[3],s=t[4],a=t[5],o=t[6],l=t[7],c=t[8],h=c*s-a*l,u=a*o-c*r,d=l*r-s*o,p=e*h+n*u+i*d;if(0===p)return this.set(0,0,0,0,0,0,0,0,0);const m=1/p;return t[0]=h*m,t[1]=(i*l-c*n)*m,t[2]=(a*n-i*s)*m,t[3]=u*m,t[4]=(c*e-i*o)*m,t[5]=(i*r-a*e)*m,t[6]=d*m,t[7]=(n*o-l*e)*m,t[8]=(s*e-n*r)*m,this}transpose(){let t;const e=this.elements;return t=e[1],e[1]=e[3],e[3]=t,t=e[2],e[2]=e[6],e[6]=t,t=e[5],e[5]=e[7],e[7]=t,this}getNormalMatrix(t){return this.setFromMatrix4(t).invert().transpose()}transposeIntoArray(t){const e=this.elements;return t[0]=e[0],t[1]=e[3],t[2]=e[6],t[3]=e[1],t[4]=e[4],t[5]=e[7],t[6]=e[2],t[7]=e[5],t[8]=e[8],this}setUvTransform(t,e,n,i,r,s,a){const o=Math.cos(r),l=Math.sin(r);return this.set(n*o,n*l,-n*(o*s+l*a)+s+t,-i*l,i*o,-i*(-l*s+o*a)+a+e,0,0,1),this}scale(t,e){return this.premultiply($n.makeScale(t,e)),this}rotate(t){return this.premultiply($n.makeRotation(-t)),this}translate(t,e){return this.premultiply($n.makeTranslation(t,e)),this}makeTranslation(t,e){return t.isVector2?this.set(1,0,t.x,0,1,t.y,0,0,1):this.set(1,0,t,0,1,e,0,0,1),this}makeRotation(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,n,e,0,0,0,1),this}makeScale(t,e){return this.set(t,0,0,0,e,0,0,0,1),this}equals(t){const e=this.elements,n=t.elements;for(let t=0;t<9;t++)if(e[t]!==n[t])return!1;return!0}fromArray(t,e=0){for(let n=0;n<9;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t}clone(){return(new this.constructor).fromArray(this.elements)}}const $n=new Kn;function Qn(t){for(let e=t.length-1;e>=0;--e)if(t[e]>=65535)return!0;return!1}const ti={Int8Array:Int8Array,Uint8Array:Uint8Array,Uint8ClampedArray:Uint8ClampedArray,Int16Array:Int16Array,Uint16Array:Uint16Array,Int32Array:Int32Array,Uint32Array:Uint32Array,Float32Array:Float32Array,Float64Array:Float64Array};function ei(t,e){return new ti[t](e)}function ni(t){return document.createElementNS("http://www.w3.org/1999/xhtml",t)}function ii(){const t=ni("canvas");return t.style.display="block",t}const ri={};function si(t){t in ri||(ri[t]=!0,console.warn(t))}const ai={enabled:!0,workingColorSpace:Ke,spaces:{},convert:function(t,e,n){return!1!==this.enabled&&e!==n&&e&&n?(this.spaces[e].transfer===Qe&&(t.r=oi(t.r),t.g=oi(t.g),t.b=oi(t.b)),this.spaces[e].primaries!==this.spaces[n].primaries&&(t.applyMatrix3(this.spaces[e].toXYZ),t.applyMatrix3(this.spaces[n].fromXYZ)),this.spaces[n].transfer===Qe&&(t.r=li(t.r),t.g=li(t.g),t.b=li(t.b)),t):t},fromWorkingColorSpace:function(t,e){return this.convert(t,this.workingColorSpace,e)},toWorkingColorSpace:function(t,e){return this.convert(t,e,this.workingColorSpace)},getPrimaries:function(t){return this.spaces[t].primaries},getTransfer:function(t){return t===Ze?$e:this.spaces[t].transfer},getLuminanceCoefficients:function(t,e=this.workingColorSpace){return t.fromArray(this.spaces[e].luminanceCoefficients)},define:function(t){Object.assign(this.spaces,t)},_getMatrix:function(t,e,n){return t.copy(this.spaces[e].toXYZ).multiply(this.spaces[n].fromXYZ)},_getDrawingBufferColorSpace:function(t){return this.spaces[t].outputColorSpaceConfig.drawingBufferColorSpace},_getUnpackColorSpace:function(t=this.workingColorSpace){return this.spaces[t].workingColorSpaceConfig.unpackColorSpace}};function oi(t){return t<.04045?.0773993808*t:Math.pow(.9478672986*t+.0521327014,2.4)}function li(t){return t<.0031308?12.92*t:1.055*Math.pow(t,.41666)-.055}const ci=[.64,.33,.3,.6,.15,.06],hi=[.2126,.7152,.0722],ui=[.3127,.329],di=(new Kn).set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),pi=(new Kn).set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);let mi;ai.define({[Ke]:{primaries:ci,whitePoint:ui,transfer:$e,toXYZ:di,fromXYZ:pi,luminanceCoefficients:hi,workingColorSpaceConfig:{unpackColorSpace:Je},outputColorSpaceConfig:{drawingBufferColorSpace:Je}},[Je]:{primaries:ci,whitePoint:ui,transfer:Qe,toXYZ:di,fromXYZ:pi,luminanceCoefficients:hi,outputColorSpaceConfig:{drawingBufferColorSpace:Je}}});class fi{static getDataURL(t){if(/^data:/i.test(t.src))return t.src;if("undefined"==typeof HTMLCanvasElement)return t.src;let e;if(t instanceof HTMLCanvasElement)e=t;else{void 0===mi&&(mi=ni("canvas")),mi.width=t.width,mi.height=t.height;const n=mi.getContext("2d");t instanceof ImageData?n.putImageData(t,0,0):n.drawImage(t,0,0,t.width,t.height),e=mi}return e.width>2048||e.height>2048?(console.warn("THREE.ImageUtils.getDataURL: Image converted to jpg for performance reasons",t),e.toDataURL("image/jpeg",.6)):e.toDataURL("image/png")}static sRGBToLinear(t){if("undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&t instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap){const e=ni("canvas");e.width=t.width,e.height=t.height;const n=e.getContext("2d");n.drawImage(t,0,0,t.width,t.height);const i=n.getImageData(0,0,t.width,t.height),r=i.data;for(let t=0;t0&&(n.userData=this.userData),e||(t.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:"dispose"})}transformUv(t){if(this.mapping!==ot)return t;if(t.applyMatrix3(this.matrix),t.x<0||t.x>1)switch(this.wrapS){case pt:t.x=t.x-Math.floor(t.x);break;case mt:t.x=t.x<0?0:1;break;case ft:1===Math.abs(Math.floor(t.x)%2)?t.x=Math.ceil(t.x)-t.x:t.x=t.x-Math.floor(t.x)}if(t.y<0||t.y>1)switch(this.wrapT){case pt:t.y=t.y-Math.floor(t.y);break;case mt:t.y=t.y<0?0:1;break;case ft:1===Math.abs(Math.floor(t.y)%2)?t.y=Math.ceil(t.y)-t.y:t.y=t.y-Math.floor(t.y)}return this.flipY&&(t.y=1-t.y),t}set needsUpdate(t){!0===t&&(this.version++,this.source.needsUpdate=!0)}set needsPMREMUpdate(t){!0===t&&this.pmremVersion++}}yi.DEFAULT_IMAGE=null,yi.DEFAULT_MAPPING=ot,yi.DEFAULT_ANISOTROPY=1;class Mi{constructor(t=0,e=0,n=0,i=1){Mi.prototype.isVector4=!0,this.x=t,this.y=e,this.z=n,this.w=i}get width(){return this.z}set width(t){this.z=t}get height(){return this.w}set height(t){this.w=t}set(t,e,n,i){return this.x=t,this.y=e,this.z=n,this.w=i,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this.w=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setW(t){return this.w=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;case 3:this.w=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this.w=void 0!==t.w?t.w:1,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this.w+=t.w,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this.w+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this.w=t.w+e.w,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this.w+=t.w*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this.w-=t.w,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this.w-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this.w=t.w-e.w,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this.w*=t.w,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this.w*=t,this}applyMatrix4(t){const e=this.x,n=this.y,i=this.z,r=this.w,s=t.elements;return this.x=s[0]*e+s[4]*n+s[8]*i+s[12]*r,this.y=s[1]*e+s[5]*n+s[9]*i+s[13]*r,this.z=s[2]*e+s[6]*n+s[10]*i+s[14]*r,this.w=s[3]*e+s[7]*n+s[11]*i+s[15]*r,this}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this.w/=t.w,this}divideScalar(t){return this.multiplyScalar(1/t)}setAxisAngleFromQuaternion(t){this.w=2*Math.acos(t.w);const e=Math.sqrt(1-t.w*t.w);return e<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=t.x/e,this.y=t.y/e,this.z=t.z/e),this}setAxisAngleFromRotationMatrix(t){let e,n,i,r;const s=.01,a=.1,o=t.elements,l=o[0],c=o[4],h=o[8],u=o[1],d=o[5],p=o[9],m=o[2],f=o[6],g=o[10];if(Math.abs(c-u)o&&t>v?tv?o=0?1:-1,i=1-e*e;if(i>Number.EPSILON){const r=Math.sqrt(i),s=Math.atan2(r,e*n);t=Math.sin(t*s)/r,a=Math.sin(a*s)/r}const r=a*n;if(o=o*t+u*r,l=l*t+d*r,c=c*t+p*r,h=h*t+m*r,t===1-a){const t=1/Math.sqrt(o*o+l*l+c*c+h*h);o*=t,l*=t,c*=t,h*=t}}t[e]=o,t[e+1]=l,t[e+2]=c,t[e+3]=h}static multiplyQuaternionsFlat(t,e,n,i,r,s){const a=n[i],o=n[i+1],l=n[i+2],c=n[i+3],h=r[s],u=r[s+1],d=r[s+2],p=r[s+3];return t[e]=a*p+c*h+o*d-l*u,t[e+1]=o*p+c*u+l*h-a*d,t[e+2]=l*p+c*d+a*u-o*h,t[e+3]=c*p-a*h-o*u-l*d,t}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get w(){return this._w}set w(t){this._w=t,this._onChangeCallback()}set(t,e,n,i){return this._x=t,this._y=e,this._z=n,this._w=i,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(t){return this._x=t.x,this._y=t.y,this._z=t.z,this._w=t.w,this._onChangeCallback(),this}setFromEuler(t,e=!0){const n=t._x,i=t._y,r=t._z,s=t._order,a=Math.cos,o=Math.sin,l=a(n/2),c=a(i/2),h=a(r/2),u=o(n/2),d=o(i/2),p=o(r/2);switch(s){case"XYZ":this._x=u*c*h+l*d*p,this._y=l*d*h-u*c*p,this._z=l*c*p+u*d*h,this._w=l*c*h-u*d*p;break;case"YXZ":this._x=u*c*h+l*d*p,this._y=l*d*h-u*c*p,this._z=l*c*p-u*d*h,this._w=l*c*h+u*d*p;break;case"ZXY":this._x=u*c*h-l*d*p,this._y=l*d*h+u*c*p,this._z=l*c*p+u*d*h,this._w=l*c*h-u*d*p;break;case"ZYX":this._x=u*c*h-l*d*p,this._y=l*d*h+u*c*p,this._z=l*c*p-u*d*h,this._w=l*c*h+u*d*p;break;case"YZX":this._x=u*c*h+l*d*p,this._y=l*d*h+u*c*p,this._z=l*c*p-u*d*h,this._w=l*c*h-u*d*p;break;case"XZY":this._x=u*c*h-l*d*p,this._y=l*d*h-u*c*p,this._z=l*c*p+u*d*h,this._w=l*c*h+u*d*p;break;default:console.warn("THREE.Quaternion: .setFromEuler() encountered an unknown order: "+s)}return!0===e&&this._onChangeCallback(),this}setFromAxisAngle(t,e){const n=e/2,i=Math.sin(n);return this._x=t.x*i,this._y=t.y*i,this._z=t.z*i,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(t){const e=t.elements,n=e[0],i=e[4],r=e[8],s=e[1],a=e[5],o=e[9],l=e[2],c=e[6],h=e[10],u=n+a+h;if(u>0){const t=.5/Math.sqrt(u+1);this._w=.25/t,this._x=(c-o)*t,this._y=(r-l)*t,this._z=(s-i)*t}else if(n>a&&n>h){const t=2*Math.sqrt(1+n-a-h);this._w=(c-o)/t,this._x=.25*t,this._y=(i+s)/t,this._z=(r+l)/t}else if(a>h){const t=2*Math.sqrt(1+a-n-h);this._w=(r-l)/t,this._x=(i+s)/t,this._y=.25*t,this._z=(o+c)/t}else{const t=2*Math.sqrt(1+h-n-a);this._w=(s-i)/t,this._x=(r+l)/t,this._y=(o+c)/t,this._z=.25*t}return this._onChangeCallback(),this}setFromUnitVectors(t,e){let n=t.dot(e)+1;return nMath.abs(t.z)?(this._x=-t.y,this._y=t.x,this._z=0,this._w=n):(this._x=0,this._y=-t.z,this._z=t.y,this._w=n)):(this._x=t.y*e.z-t.z*e.y,this._y=t.z*e.x-t.x*e.z,this._z=t.x*e.y-t.y*e.x,this._w=n),this.normalize()}angleTo(t){return 2*Math.acos(Math.abs(Wn(this.dot(t),-1,1)))}rotateTowards(t,e){const n=this.angleTo(t);if(0===n)return this;const i=Math.min(1,e/n);return this.slerp(t,i),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(t){return this._x*t._x+this._y*t._y+this._z*t._z+this._w*t._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let t=this.length();return 0===t?(this._x=0,this._y=0,this._z=0,this._w=1):(t=1/t,this._x=this._x*t,this._y=this._y*t,this._z=this._z*t,this._w=this._w*t),this._onChangeCallback(),this}multiply(t){return this.multiplyQuaternions(this,t)}premultiply(t){return this.multiplyQuaternions(t,this)}multiplyQuaternions(t,e){const n=t._x,i=t._y,r=t._z,s=t._w,a=e._x,o=e._y,l=e._z,c=e._w;return this._x=n*c+s*a+i*l-r*o,this._y=i*c+s*o+r*a-n*l,this._z=r*c+s*l+n*o-i*a,this._w=s*c-n*a-i*o-r*l,this._onChangeCallback(),this}slerp(t,e){if(0===e)return this;if(1===e)return this.copy(t);const n=this._x,i=this._y,r=this._z,s=this._w;let a=s*t._w+n*t._x+i*t._y+r*t._z;if(a<0?(this._w=-t._w,this._x=-t._x,this._y=-t._y,this._z=-t._z,a=-a):this.copy(t),a>=1)return this._w=s,this._x=n,this._y=i,this._z=r,this;const o=1-a*a;if(o<=Number.EPSILON){const t=1-e;return this._w=t*s+e*this._w,this._x=t*n+e*this._x,this._y=t*i+e*this._y,this._z=t*r+e*this._z,this.normalize(),this}const l=Math.sqrt(o),c=Math.atan2(l,a),h=Math.sin((1-e)*c)/l,u=Math.sin(e*c)/l;return this._w=s*h+this._w*u,this._x=n*h+this._x*u,this._y=i*h+this._y*u,this._z=r*h+this._z*u,this._onChangeCallback(),this}slerpQuaternions(t,e,n){return this.copy(t).slerp(e,n)}random(){const t=2*Math.PI*Math.random(),e=2*Math.PI*Math.random(),n=Math.random(),i=Math.sqrt(1-n),r=Math.sqrt(n);return this.set(i*Math.sin(t),i*Math.cos(t),r*Math.sin(e),r*Math.cos(e))}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._w===this._w}fromArray(t,e=0){return this._x=t[e],this._y=t[e+1],this._z=t[e+2],this._w=t[e+3],this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._w,t}fromBufferAttribute(t,e){return this._x=t.getX(e),this._y=t.getY(e),this._z=t.getZ(e),this._w=t.getW(e),this._onChangeCallback(),this}toJSON(){return this.toArray()}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}}class Ci{constructor(t=0,e=0,n=0){Ci.prototype.isVector3=!0,this.x=t,this.y=e,this.z=n}set(t,e,n){return void 0===n&&(n=this.z),this.x=t,this.y=e,this.z=n,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this}multiplyVectors(t,e){return this.x=t.x*e.x,this.y=t.y*e.y,this.z=t.z*e.z,this}applyEuler(t){return this.applyQuaternion(Pi.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(Pi.setFromAxisAngle(t,e))}applyMatrix3(t){const e=this.x,n=this.y,i=this.z,r=t.elements;return this.x=r[0]*e+r[3]*n+r[6]*i,this.y=r[1]*e+r[4]*n+r[7]*i,this.z=r[2]*e+r[5]*n+r[8]*i,this}applyNormalMatrix(t){return this.applyMatrix3(t).normalize()}applyMatrix4(t){const e=this.x,n=this.y,i=this.z,r=t.elements,s=1/(r[3]*e+r[7]*n+r[11]*i+r[15]);return this.x=(r[0]*e+r[4]*n+r[8]*i+r[12])*s,this.y=(r[1]*e+r[5]*n+r[9]*i+r[13])*s,this.z=(r[2]*e+r[6]*n+r[10]*i+r[14])*s,this}applyQuaternion(t){const e=this.x,n=this.y,i=this.z,r=t.x,s=t.y,a=t.z,o=t.w,l=2*(s*i-a*n),c=2*(a*e-r*i),h=2*(r*n-s*e);return this.x=e+o*l+s*h-a*c,this.y=n+o*c+a*l-r*h,this.z=i+o*h+r*c-s*l,this}project(t){return this.applyMatrix4(t.matrixWorldInverse).applyMatrix4(t.projectionMatrix)}unproject(t){return this.applyMatrix4(t.projectionMatrixInverse).applyMatrix4(t.matrixWorld)}transformDirection(t){const e=this.x,n=this.y,i=this.z,r=t.elements;return this.x=r[0]*e+r[4]*n+r[8]*i,this.y=r[1]*e+r[5]*n+r[9]*i,this.z=r[2]*e+r[6]*n+r[10]*i,this.normalize()}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this}divideScalar(t){return this.multiplyScalar(1/t)}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this}clamp(t,e){return this.x=Math.max(t.x,Math.min(e.x,this.x)),this.y=Math.max(t.y,Math.min(e.y,this.y)),this.z=Math.max(t.z,Math.min(e.z,this.z)),this}clampScalar(t,e){return this.x=Math.max(t,Math.min(e,this.x)),this.y=Math.max(t,Math.min(e,this.y)),this.z=Math.max(t,Math.min(e,this.z)),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Math.max(t,Math.min(e,n)))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this.z=t.z+(e.z-t.z)*n,this}cross(t){return this.crossVectors(this,t)}crossVectors(t,e){const n=t.x,i=t.y,r=t.z,s=e.x,a=e.y,o=e.z;return this.x=i*o-r*a,this.y=r*s-n*o,this.z=n*a-i*s,this}projectOnVector(t){const e=t.lengthSq();if(0===e)return this.set(0,0,0);const n=t.dot(this)/e;return this.copy(t).multiplyScalar(n)}projectOnPlane(t){return Ii.copy(this).projectOnVector(t),this.sub(Ii)}reflect(t){return this.sub(Ii.copy(t).multiplyScalar(2*this.dot(t)))}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(0===e)return Math.PI/2;const n=this.dot(t)/e;return Math.acos(Wn(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y,i=this.z-t.z;return e*e+n*n+i*i}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)+Math.abs(this.z-t.z)}setFromSpherical(t){return this.setFromSphericalCoords(t.radius,t.phi,t.theta)}setFromSphericalCoords(t,e,n){const i=Math.sin(e)*t;return this.x=i*Math.sin(n),this.y=Math.cos(e)*t,this.z=i*Math.cos(n),this}setFromCylindrical(t){return this.setFromCylindricalCoords(t.radius,t.theta,t.y)}setFromCylindricalCoords(t,e,n){return this.x=t*Math.sin(e),this.y=n,this.z=t*Math.cos(e),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this}setFromMatrixScale(t){const e=this.setFromMatrixColumn(t,0).length(),n=this.setFromMatrixColumn(t,1).length(),i=this.setFromMatrixColumn(t,2).length();return this.x=e,this.y=n,this.z=i,this}setFromMatrixColumn(t,e){return this.fromArray(t.elements,4*e)}setFromMatrix3Column(t,e){return this.fromArray(t.elements,3*e)}setFromEuler(t){return this.x=t._x,this.y=t._y,this.z=t._z,this}setFromColor(t){return this.x=t.r,this.y=t.g,this.z=t.b,this}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){const t=Math.random()*Math.PI*2,e=2*Math.random()-1,n=Math.sqrt(1-e*e);return this.x=n*Math.cos(t),this.y=e,this.z=n*Math.sin(t),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}}const Ii=new Ci,Pi=new Ri;class Li{constructor(t=new Ci(1/0,1/0,1/0),e=new Ci(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=t,this.max=e}set(t,e){return this.min.copy(t),this.max.copy(e),this}setFromArray(t){this.makeEmpty();for(let e=0,n=t.length;e=this.min.x&&t.x<=this.max.x&&t.y>=this.min.y&&t.y<=this.max.y&&t.z>=this.min.z&&t.z<=this.max.z}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y&&this.min.z<=t.min.z&&t.max.z<=this.max.z}getParameter(t,e){return e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y),(t.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(t){return t.max.x>=this.min.x&&t.min.x<=this.max.x&&t.max.y>=this.min.y&&t.min.y<=this.max.y&&t.max.z>=this.min.z&&t.min.z<=this.max.z}intersectsSphere(t){return this.clampPoint(t.center,Di),Di.distanceToSquared(t.center)<=t.radius*t.radius}intersectsPlane(t){let e,n;return t.normal.x>0?(e=t.normal.x*this.min.x,n=t.normal.x*this.max.x):(e=t.normal.x*this.max.x,n=t.normal.x*this.min.x),t.normal.y>0?(e+=t.normal.y*this.min.y,n+=t.normal.y*this.max.y):(e+=t.normal.y*this.max.y,n+=t.normal.y*this.min.y),t.normal.z>0?(e+=t.normal.z*this.min.z,n+=t.normal.z*this.max.z):(e+=t.normal.z*this.max.z,n+=t.normal.z*this.min.z),e<=-t.constant&&n>=-t.constant}intersectsTriangle(t){if(this.isEmpty())return!1;this.getCenter(Hi),Gi.subVectors(this.max,Hi),Oi.subVectors(t.a,Hi),Fi.subVectors(t.b,Hi),Bi.subVectors(t.c,Hi),zi.subVectors(Fi,Oi),ki.subVectors(Bi,Fi),Vi.subVectors(Oi,Bi);let e=[0,-zi.z,zi.y,0,-ki.z,ki.y,0,-Vi.z,Vi.y,zi.z,0,-zi.x,ki.z,0,-ki.x,Vi.z,0,-Vi.x,-zi.y,zi.x,0,-ki.y,ki.x,0,-Vi.y,Vi.x,0];return!!ji(e,Oi,Fi,Bi,Gi)&&(e=[1,0,0,0,1,0,0,0,1],!!ji(e,Oi,Fi,Bi,Gi)&&(Wi.crossVectors(zi,ki),e=[Wi.x,Wi.y,Wi.z],ji(e,Oi,Fi,Bi,Gi)))}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return this.clampPoint(t,Di).distanceTo(t)}getBoundingSphere(t){return this.isEmpty()?t.makeEmpty():(this.getCenter(t.center),t.radius=.5*this.getSize(Di).length()),t}intersect(t){return this.min.max(t.min),this.max.min(t.max),this.isEmpty()&&this.makeEmpty(),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}applyMatrix4(t){return this.isEmpty()||(Ui[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),Ui[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),Ui[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),Ui[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),Ui[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),Ui[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),Ui[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),Ui[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(Ui)),this}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}}const Ui=[new Ci,new Ci,new Ci,new Ci,new Ci,new Ci,new Ci,new Ci],Di=new Ci,Ni=new Li,Oi=new Ci,Fi=new Ci,Bi=new Ci,zi=new Ci,ki=new Ci,Vi=new Ci,Hi=new Ci,Gi=new Ci,Wi=new Ci,Xi=new Ci;function ji(t,e,n,i,r){for(let s=0,a=t.length-3;s<=a;s+=3){Xi.fromArray(t,s);const a=r.x*Math.abs(Xi.x)+r.y*Math.abs(Xi.y)+r.z*Math.abs(Xi.z),o=e.dot(Xi),l=n.dot(Xi),c=i.dot(Xi);if(Math.max(-Math.max(o,l,c),Math.min(o,l,c))>a)return!1}return!0}const qi=new Li,Yi=new Ci,Zi=new Ci;class Ji{constructor(t=new Ci,e=-1){this.isSphere=!0,this.center=t,this.radius=e}set(t,e){return this.center.copy(t),this.radius=e,this}setFromPoints(t,e){const n=this.center;void 0!==e?n.copy(e):qi.setFromPoints(t).getCenter(n);let i=0;for(let e=0,r=t.length;ethis.radius*this.radius&&(e.sub(this.center).normalize(),e.multiplyScalar(this.radius).add(this.center)),e}getBoundingBox(t){return this.isEmpty()?(t.makeEmpty(),t):(t.set(this.center,this.center),t.expandByScalar(this.radius),t)}applyMatrix4(t){return this.center.applyMatrix4(t),this.radius=this.radius*t.getMaxScaleOnAxis(),this}translate(t){return this.center.add(t),this}expandByPoint(t){if(this.isEmpty())return this.center.copy(t),this.radius=0,this;Yi.subVectors(t,this.center);const e=Yi.lengthSq();if(e>this.radius*this.radius){const t=Math.sqrt(e),n=.5*(t-this.radius);this.center.addScaledVector(Yi,n/t),this.radius+=n}return this}union(t){return t.isEmpty()?this:this.isEmpty()?(this.copy(t),this):(!0===this.center.equals(t.center)?this.radius=Math.max(this.radius,t.radius):(Zi.subVectors(t.center,this.center).setLength(t.radius),this.expandByPoint(Yi.copy(t.center).add(Zi)),this.expandByPoint(Yi.copy(t.center).sub(Zi))),this)}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return(new this.constructor).copy(this)}}const Ki=new Ci,$i=new Ci,Qi=new Ci,tr=new Ci,er=new Ci,nr=new Ci,ir=new Ci;class rr{constructor(t=new Ci,e=new Ci(0,0,-1)){this.origin=t,this.direction=e}set(t,e){return this.origin.copy(t),this.direction.copy(e),this}copy(t){return this.origin.copy(t.origin),this.direction.copy(t.direction),this}at(t,e){return e.copy(this.origin).addScaledVector(this.direction,t)}lookAt(t){return this.direction.copy(t).sub(this.origin).normalize(),this}recast(t){return this.origin.copy(this.at(t,Ki)),this}closestPointToPoint(t,e){e.subVectors(t,this.origin);const n=e.dot(this.direction);return n<0?e.copy(this.origin):e.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(t){return Math.sqrt(this.distanceSqToPoint(t))}distanceSqToPoint(t){const e=Ki.subVectors(t,this.origin).dot(this.direction);return e<0?this.origin.distanceToSquared(t):(Ki.copy(this.origin).addScaledVector(this.direction,e),Ki.distanceToSquared(t))}distanceSqToSegment(t,e,n,i){$i.copy(t).add(e).multiplyScalar(.5),Qi.copy(e).sub(t).normalize(),tr.copy(this.origin).sub($i);const r=.5*t.distanceTo(e),s=-this.direction.dot(Qi),a=tr.dot(this.direction),o=-tr.dot(Qi),l=tr.lengthSq(),c=Math.abs(1-s*s);let h,u,d,p;if(c>0)if(h=s*o-a,u=s*a-o,p=r*c,h>=0)if(u>=-p)if(u<=p){const t=1/c;h*=t,u*=t,d=h*(h+s*u+2*a)+u*(s*h+u+2*o)+l}else u=r,h=Math.max(0,-(s*u+a)),d=-h*h+u*(u+2*o)+l;else u=-r,h=Math.max(0,-(s*u+a)),d=-h*h+u*(u+2*o)+l;else u<=-p?(h=Math.max(0,-(-s*r+a)),u=h>0?-r:Math.min(Math.max(-r,-o),r),d=-h*h+u*(u+2*o)+l):u<=p?(h=0,u=Math.min(Math.max(-r,-o),r),d=u*(u+2*o)+l):(h=Math.max(0,-(s*r+a)),u=h>0?r:Math.min(Math.max(-r,-o),r),d=-h*h+u*(u+2*o)+l);else u=s>0?-r:r,h=Math.max(0,-(s*u+a)),d=-h*h+u*(u+2*o)+l;return n&&n.copy(this.origin).addScaledVector(this.direction,h),i&&i.copy($i).addScaledVector(Qi,u),d}intersectSphere(t,e){Ki.subVectors(t.center,this.origin);const n=Ki.dot(this.direction),i=Ki.dot(Ki)-n*n,r=t.radius*t.radius;if(i>r)return null;const s=Math.sqrt(r-i),a=n-s,o=n+s;return o<0?null:a<0?this.at(o,e):this.at(a,e)}intersectsSphere(t){return this.distanceSqToPoint(t.center)<=t.radius*t.radius}distanceToPlane(t){const e=t.normal.dot(this.direction);if(0===e)return 0===t.distanceToPoint(this.origin)?0:null;const n=-(this.origin.dot(t.normal)+t.constant)/e;return n>=0?n:null}intersectPlane(t,e){const n=this.distanceToPlane(t);return null===n?null:this.at(n,e)}intersectsPlane(t){const e=t.distanceToPoint(this.origin);if(0===e)return!0;return t.normal.dot(this.direction)*e<0}intersectBox(t,e){let n,i,r,s,a,o;const l=1/this.direction.x,c=1/this.direction.y,h=1/this.direction.z,u=this.origin;return l>=0?(n=(t.min.x-u.x)*l,i=(t.max.x-u.x)*l):(n=(t.max.x-u.x)*l,i=(t.min.x-u.x)*l),c>=0?(r=(t.min.y-u.y)*c,s=(t.max.y-u.y)*c):(r=(t.max.y-u.y)*c,s=(t.min.y-u.y)*c),n>s||r>i?null:((r>n||isNaN(n))&&(n=r),(s=0?(a=(t.min.z-u.z)*h,o=(t.max.z-u.z)*h):(a=(t.max.z-u.z)*h,o=(t.min.z-u.z)*h),n>o||a>i?null:((a>n||n!=n)&&(n=a),(o=0?n:i,e)))}intersectsBox(t){return null!==this.intersectBox(t,Ki)}intersectTriangle(t,e,n,i,r){er.subVectors(e,t),nr.subVectors(n,t),ir.crossVectors(er,nr);let s,a=this.direction.dot(ir);if(a>0){if(i)return null;s=1}else{if(!(a<0))return null;s=-1,a=-a}tr.subVectors(this.origin,t);const o=s*this.direction.dot(nr.crossVectors(tr,nr));if(o<0)return null;const l=s*this.direction.dot(er.cross(tr));if(l<0)return null;if(o+l>a)return null;const c=-s*tr.dot(ir);return c<0?null:this.at(c/a,r)}applyMatrix4(t){return this.origin.applyMatrix4(t),this.direction.transformDirection(t),this}equals(t){return t.origin.equals(this.origin)&&t.direction.equals(this.direction)}clone(){return(new this.constructor).copy(this)}}class sr{constructor(t,e,n,i,r,s,a,o,l,c,h,u,d,p,m,f){sr.prototype.isMatrix4=!0,this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],void 0!==t&&this.set(t,e,n,i,r,s,a,o,l,c,h,u,d,p,m,f)}set(t,e,n,i,r,s,a,o,l,c,h,u,d,p,m,f){const g=this.elements;return g[0]=t,g[4]=e,g[8]=n,g[12]=i,g[1]=r,g[5]=s,g[9]=a,g[13]=o,g[2]=l,g[6]=c,g[10]=h,g[14]=u,g[3]=d,g[7]=p,g[11]=m,g[15]=f,this}identity(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this}clone(){return(new sr).fromArray(this.elements)}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],e[9]=n[9],e[10]=n[10],e[11]=n[11],e[12]=n[12],e[13]=n[13],e[14]=n[14],e[15]=n[15],this}copyPosition(t){const e=this.elements,n=t.elements;return e[12]=n[12],e[13]=n[13],e[14]=n[14],this}setFromMatrix3(t){const e=t.elements;return this.set(e[0],e[3],e[6],0,e[1],e[4],e[7],0,e[2],e[5],e[8],0,0,0,0,1),this}extractBasis(t,e,n){return t.setFromMatrixColumn(this,0),e.setFromMatrixColumn(this,1),n.setFromMatrixColumn(this,2),this}makeBasis(t,e,n){return this.set(t.x,e.x,n.x,0,t.y,e.y,n.y,0,t.z,e.z,n.z,0,0,0,0,1),this}extractRotation(t){const e=this.elements,n=t.elements,i=1/ar.setFromMatrixColumn(t,0).length(),r=1/ar.setFromMatrixColumn(t,1).length(),s=1/ar.setFromMatrixColumn(t,2).length();return e[0]=n[0]*i,e[1]=n[1]*i,e[2]=n[2]*i,e[3]=0,e[4]=n[4]*r,e[5]=n[5]*r,e[6]=n[6]*r,e[7]=0,e[8]=n[8]*s,e[9]=n[9]*s,e[10]=n[10]*s,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromEuler(t){const e=this.elements,n=t.x,i=t.y,r=t.z,s=Math.cos(n),a=Math.sin(n),o=Math.cos(i),l=Math.sin(i),c=Math.cos(r),h=Math.sin(r);if("XYZ"===t.order){const t=s*c,n=s*h,i=a*c,r=a*h;e[0]=o*c,e[4]=-o*h,e[8]=l,e[1]=n+i*l,e[5]=t-r*l,e[9]=-a*o,e[2]=r-t*l,e[6]=i+n*l,e[10]=s*o}else if("YXZ"===t.order){const t=o*c,n=o*h,i=l*c,r=l*h;e[0]=t+r*a,e[4]=i*a-n,e[8]=s*l,e[1]=s*h,e[5]=s*c,e[9]=-a,e[2]=n*a-i,e[6]=r+t*a,e[10]=s*o}else if("ZXY"===t.order){const t=o*c,n=o*h,i=l*c,r=l*h;e[0]=t-r*a,e[4]=-s*h,e[8]=i+n*a,e[1]=n+i*a,e[5]=s*c,e[9]=r-t*a,e[2]=-s*l,e[6]=a,e[10]=s*o}else if("ZYX"===t.order){const t=s*c,n=s*h,i=a*c,r=a*h;e[0]=o*c,e[4]=i*l-n,e[8]=t*l+r,e[1]=o*h,e[5]=r*l+t,e[9]=n*l-i,e[2]=-l,e[6]=a*o,e[10]=s*o}else if("YZX"===t.order){const t=s*o,n=s*l,i=a*o,r=a*l;e[0]=o*c,e[4]=r-t*h,e[8]=i*h+n,e[1]=h,e[5]=s*c,e[9]=-a*c,e[2]=-l*c,e[6]=n*h+i,e[10]=t-r*h}else if("XZY"===t.order){const t=s*o,n=s*l,i=a*o,r=a*l;e[0]=o*c,e[4]=-h,e[8]=l*c,e[1]=t*h+r,e[5]=s*c,e[9]=n*h-i,e[2]=i*h-n,e[6]=a*c,e[10]=r*h+t}return e[3]=0,e[7]=0,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromQuaternion(t){return this.compose(lr,t,cr)}lookAt(t,e,n){const i=this.elements;return dr.subVectors(t,e),0===dr.lengthSq()&&(dr.z=1),dr.normalize(),hr.crossVectors(n,dr),0===hr.lengthSq()&&(1===Math.abs(n.z)?dr.x+=1e-4:dr.z+=1e-4,dr.normalize(),hr.crossVectors(n,dr)),hr.normalize(),ur.crossVectors(dr,hr),i[0]=hr.x,i[4]=ur.x,i[8]=dr.x,i[1]=hr.y,i[5]=ur.y,i[9]=dr.y,i[2]=hr.z,i[6]=ur.z,i[10]=dr.z,this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,i=e.elements,r=this.elements,s=n[0],a=n[4],o=n[8],l=n[12],c=n[1],h=n[5],u=n[9],d=n[13],p=n[2],m=n[6],f=n[10],g=n[14],v=n[3],_=n[7],x=n[11],y=n[15],M=i[0],S=i[4],b=i[8],T=i[12],w=i[1],E=i[5],A=i[9],R=i[13],C=i[2],I=i[6],P=i[10],L=i[14],U=i[3],D=i[7],N=i[11],O=i[15];return r[0]=s*M+a*w+o*C+l*U,r[4]=s*S+a*E+o*I+l*D,r[8]=s*b+a*A+o*P+l*N,r[12]=s*T+a*R+o*L+l*O,r[1]=c*M+h*w+u*C+d*U,r[5]=c*S+h*E+u*I+d*D,r[9]=c*b+h*A+u*P+d*N,r[13]=c*T+h*R+u*L+d*O,r[2]=p*M+m*w+f*C+g*U,r[6]=p*S+m*E+f*I+g*D,r[10]=p*b+m*A+f*P+g*N,r[14]=p*T+m*R+f*L+g*O,r[3]=v*M+_*w+x*C+y*U,r[7]=v*S+_*E+x*I+y*D,r[11]=v*b+_*A+x*P+y*N,r[15]=v*T+_*R+x*L+y*O,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[4]*=t,e[8]*=t,e[12]*=t,e[1]*=t,e[5]*=t,e[9]*=t,e[13]*=t,e[2]*=t,e[6]*=t,e[10]*=t,e[14]*=t,e[3]*=t,e[7]*=t,e[11]*=t,e[15]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[4],i=t[8],r=t[12],s=t[1],a=t[5],o=t[9],l=t[13],c=t[2],h=t[6],u=t[10],d=t[14];return t[3]*(+r*o*h-i*l*h-r*a*u+n*l*u+i*a*d-n*o*d)+t[7]*(+e*o*d-e*l*u+r*s*u-i*s*d+i*l*c-r*o*c)+t[11]*(+e*l*h-e*a*d-r*s*h+n*s*d+r*a*c-n*l*c)+t[15]*(-i*a*c-e*o*h+e*a*u+i*s*h-n*s*u+n*o*c)}transpose(){const t=this.elements;let e;return e=t[1],t[1]=t[4],t[4]=e,e=t[2],t[2]=t[8],t[8]=e,e=t[6],t[6]=t[9],t[9]=e,e=t[3],t[3]=t[12],t[12]=e,e=t[7],t[7]=t[13],t[13]=e,e=t[11],t[11]=t[14],t[14]=e,this}setPosition(t,e,n){const i=this.elements;return t.isVector3?(i[12]=t.x,i[13]=t.y,i[14]=t.z):(i[12]=t,i[13]=e,i[14]=n),this}invert(){const t=this.elements,e=t[0],n=t[1],i=t[2],r=t[3],s=t[4],a=t[5],o=t[6],l=t[7],c=t[8],h=t[9],u=t[10],d=t[11],p=t[12],m=t[13],f=t[14],g=t[15],v=h*f*l-m*u*l+m*o*d-a*f*d-h*o*g+a*u*g,_=p*u*l-c*f*l-p*o*d+s*f*d+c*o*g-s*u*g,x=c*m*l-p*h*l+p*a*d-s*m*d-c*a*g+s*h*g,y=p*h*o-c*m*o-p*a*u+s*m*u+c*a*f-s*h*f,M=e*v+n*_+i*x+r*y;if(0===M)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const S=1/M;return t[0]=v*S,t[1]=(m*u*r-h*f*r-m*i*d+n*f*d+h*i*g-n*u*g)*S,t[2]=(a*f*r-m*o*r+m*i*l-n*f*l-a*i*g+n*o*g)*S,t[3]=(h*o*r-a*u*r-h*i*l+n*u*l+a*i*d-n*o*d)*S,t[4]=_*S,t[5]=(c*f*r-p*u*r+p*i*d-e*f*d-c*i*g+e*u*g)*S,t[6]=(p*o*r-s*f*r-p*i*l+e*f*l+s*i*g-e*o*g)*S,t[7]=(s*u*r-c*o*r+c*i*l-e*u*l-s*i*d+e*o*d)*S,t[8]=x*S,t[9]=(p*h*r-c*m*r-p*n*d+e*m*d+c*n*g-e*h*g)*S,t[10]=(s*m*r-p*a*r+p*n*l-e*m*l-s*n*g+e*a*g)*S,t[11]=(c*a*r-s*h*r-c*n*l+e*h*l+s*n*d-e*a*d)*S,t[12]=y*S,t[13]=(c*m*i-p*h*i+p*n*u-e*m*u-c*n*f+e*h*f)*S,t[14]=(p*a*i-s*m*i-p*n*o+e*m*o+s*n*f-e*a*f)*S,t[15]=(s*h*i-c*a*i+c*n*o-e*h*o-s*n*u+e*a*u)*S,this}scale(t){const e=this.elements,n=t.x,i=t.y,r=t.z;return e[0]*=n,e[4]*=i,e[8]*=r,e[1]*=n,e[5]*=i,e[9]*=r,e[2]*=n,e[6]*=i,e[10]*=r,e[3]*=n,e[7]*=i,e[11]*=r,this}getMaxScaleOnAxis(){const t=this.elements,e=t[0]*t[0]+t[1]*t[1]+t[2]*t[2],n=t[4]*t[4]+t[5]*t[5]+t[6]*t[6],i=t[8]*t[8]+t[9]*t[9]+t[10]*t[10];return Math.sqrt(Math.max(e,n,i))}makeTranslation(t,e,n){return t.isVector3?this.set(1,0,0,t.x,0,1,0,t.y,0,0,1,t.z,0,0,0,1):this.set(1,0,0,t,0,1,0,e,0,0,1,n,0,0,0,1),this}makeRotationX(t){const e=Math.cos(t),n=Math.sin(t);return this.set(1,0,0,0,0,e,-n,0,0,n,e,0,0,0,0,1),this}makeRotationY(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,0,n,0,0,1,0,0,-n,0,e,0,0,0,0,1),this}makeRotationZ(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,0,n,e,0,0,0,0,1,0,0,0,0,1),this}makeRotationAxis(t,e){const n=Math.cos(e),i=Math.sin(e),r=1-n,s=t.x,a=t.y,o=t.z,l=r*s,c=r*a;return this.set(l*s+n,l*a-i*o,l*o+i*a,0,l*a+i*o,c*a+n,c*o-i*s,0,l*o-i*a,c*o+i*s,r*o*o+n,0,0,0,0,1),this}makeScale(t,e,n){return this.set(t,0,0,0,0,e,0,0,0,0,n,0,0,0,0,1),this}makeShear(t,e,n,i,r,s){return this.set(1,n,r,0,t,1,s,0,e,i,1,0,0,0,0,1),this}compose(t,e,n){const i=this.elements,r=e._x,s=e._y,a=e._z,o=e._w,l=r+r,c=s+s,h=a+a,u=r*l,d=r*c,p=r*h,m=s*c,f=s*h,g=a*h,v=o*l,_=o*c,x=o*h,y=n.x,M=n.y,S=n.z;return i[0]=(1-(m+g))*y,i[1]=(d+x)*y,i[2]=(p-_)*y,i[3]=0,i[4]=(d-x)*M,i[5]=(1-(u+g))*M,i[6]=(f+v)*M,i[7]=0,i[8]=(p+_)*S,i[9]=(f-v)*S,i[10]=(1-(u+m))*S,i[11]=0,i[12]=t.x,i[13]=t.y,i[14]=t.z,i[15]=1,this}decompose(t,e,n){const i=this.elements;let r=ar.set(i[0],i[1],i[2]).length();const s=ar.set(i[4],i[5],i[6]).length(),a=ar.set(i[8],i[9],i[10]).length();this.determinant()<0&&(r=-r),t.x=i[12],t.y=i[13],t.z=i[14],or.copy(this);const o=1/r,l=1/s,c=1/a;return or.elements[0]*=o,or.elements[1]*=o,or.elements[2]*=o,or.elements[4]*=l,or.elements[5]*=l,or.elements[6]*=l,or.elements[8]*=c,or.elements[9]*=c,or.elements[10]*=c,e.setFromRotationMatrix(or),n.x=r,n.y=s,n.z=a,this}makePerspective(t,e,n,i,r,s,a=2e3){const o=this.elements,l=2*r/(e-t),c=2*r/(n-i),h=(e+t)/(e-t),u=(n+i)/(n-i);let d,p;if(a===On)d=-(s+r)/(s-r),p=-2*s*r/(s-r);else{if(a!==Fn)throw new Error("THREE.Matrix4.makePerspective(): Invalid coordinate system: "+a);d=-s/(s-r),p=-s*r/(s-r)}return o[0]=l,o[4]=0,o[8]=h,o[12]=0,o[1]=0,o[5]=c,o[9]=u,o[13]=0,o[2]=0,o[6]=0,o[10]=d,o[14]=p,o[3]=0,o[7]=0,o[11]=-1,o[15]=0,this}makeOrthographic(t,e,n,i,r,s,a=2e3){const o=this.elements,l=1/(e-t),c=1/(n-i),h=1/(s-r),u=(e+t)*l,d=(n+i)*c;let p,m;if(a===On)p=(s+r)*h,m=-2*h;else{if(a!==Fn)throw new Error("THREE.Matrix4.makeOrthographic(): Invalid coordinate system: "+a);p=r*h,m=-1*h}return o[0]=2*l,o[4]=0,o[8]=0,o[12]=-u,o[1]=0,o[5]=2*c,o[9]=0,o[13]=-d,o[2]=0,o[6]=0,o[10]=m,o[14]=-p,o[3]=0,o[7]=0,o[11]=0,o[15]=1,this}equals(t){const e=this.elements,n=t.elements;for(let t=0;t<16;t++)if(e[t]!==n[t])return!1;return!0}fromArray(t,e=0){for(let n=0;n<16;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t[e+9]=n[9],t[e+10]=n[10],t[e+11]=n[11],t[e+12]=n[12],t[e+13]=n[13],t[e+14]=n[14],t[e+15]=n[15],t}}const ar=new Ci,or=new sr,lr=new Ci(0,0,0),cr=new Ci(1,1,1),hr=new Ci,ur=new Ci,dr=new Ci,pr=new sr,mr=new Ri;class fr{constructor(t=0,e=0,n=0,i=fr.DEFAULT_ORDER){this.isEuler=!0,this._x=t,this._y=e,this._z=n,this._order=i}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get order(){return this._order}set order(t){this._order=t,this._onChangeCallback()}set(t,e,n,i=this._order){return this._x=t,this._y=e,this._z=n,this._order=i,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._order)}copy(t){return this._x=t._x,this._y=t._y,this._z=t._z,this._order=t._order,this._onChangeCallback(),this}setFromRotationMatrix(t,e=this._order,n=!0){const i=t.elements,r=i[0],s=i[4],a=i[8],o=i[1],l=i[5],c=i[9],h=i[2],u=i[6],d=i[10];switch(e){case"XYZ":this._y=Math.asin(Wn(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(-c,d),this._z=Math.atan2(-s,r)):(this._x=Math.atan2(u,l),this._z=0);break;case"YXZ":this._x=Math.asin(-Wn(c,-1,1)),Math.abs(c)<.9999999?(this._y=Math.atan2(a,d),this._z=Math.atan2(o,l)):(this._y=Math.atan2(-h,r),this._z=0);break;case"ZXY":this._x=Math.asin(Wn(u,-1,1)),Math.abs(u)<.9999999?(this._y=Math.atan2(-h,d),this._z=Math.atan2(-s,l)):(this._y=0,this._z=Math.atan2(o,r));break;case"ZYX":this._y=Math.asin(-Wn(h,-1,1)),Math.abs(h)<.9999999?(this._x=Math.atan2(u,d),this._z=Math.atan2(o,r)):(this._x=0,this._z=Math.atan2(-s,l));break;case"YZX":this._z=Math.asin(Wn(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-c,l),this._y=Math.atan2(-h,r)):(this._x=0,this._y=Math.atan2(a,d));break;case"XZY":this._z=Math.asin(-Wn(s,-1,1)),Math.abs(s)<.9999999?(this._x=Math.atan2(u,l),this._y=Math.atan2(a,r)):(this._x=Math.atan2(-c,d),this._y=0);break;default:console.warn("THREE.Euler: .setFromRotationMatrix() encountered an unknown order: "+e)}return this._order=e,!0===n&&this._onChangeCallback(),this}setFromQuaternion(t,e,n){return pr.makeRotationFromQuaternion(t),this.setFromRotationMatrix(pr,e,n)}setFromVector3(t,e=this._order){return this.set(t.x,t.y,t.z,e)}reorder(t){return mr.setFromEuler(this),this.setFromQuaternion(mr,t)}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._order===this._order}fromArray(t){return this._x=t[0],this._y=t[1],this._z=t[2],void 0!==t[3]&&(this._order=t[3]),this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._order,t}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._order}}fr.DEFAULT_ORDER="XYZ";class gr{constructor(){this.mask=1}set(t){this.mask=1<>>0}enable(t){this.mask|=1<1){for(let t=0;t1){for(let t=0;t0&&(i.userData=this.userData),i.layers=this.layers.mask,i.matrix=this.matrix.toArray(),i.up=this.up.toArray(),!1===this.matrixAutoUpdate&&(i.matrixAutoUpdate=!1),this.isInstancedMesh&&(i.type="InstancedMesh",i.count=this.count,i.instanceMatrix=this.instanceMatrix.toJSON(),null!==this.instanceColor&&(i.instanceColor=this.instanceColor.toJSON())),this.isBatchedMesh&&(i.type="BatchedMesh",i.perObjectFrustumCulled=this.perObjectFrustumCulled,i.sortObjects=this.sortObjects,i.drawRanges=this._drawRanges,i.reservedRanges=this._reservedRanges,i.visibility=this._visibility,i.active=this._active,i.bounds=this._bounds.map((t=>({boxInitialized:t.boxInitialized,boxMin:t.box.min.toArray(),boxMax:t.box.max.toArray(),sphereInitialized:t.sphereInitialized,sphereRadius:t.sphere.radius,sphereCenter:t.sphere.center.toArray()}))),i.maxInstanceCount=this._maxInstanceCount,i.maxVertexCount=this._maxVertexCount,i.maxIndexCount=this._maxIndexCount,i.geometryInitialized=this._geometryInitialized,i.geometryCount=this._geometryCount,i.matricesTexture=this._matricesTexture.toJSON(t),null!==this._colorsTexture&&(i.colorsTexture=this._colorsTexture.toJSON(t)),null!==this.boundingSphere&&(i.boundingSphere={center:i.boundingSphere.center.toArray(),radius:i.boundingSphere.radius}),null!==this.boundingBox&&(i.boundingBox={min:i.boundingBox.min.toArray(),max:i.boundingBox.max.toArray()})),this.isScene)this.background&&(this.background.isColor?i.background=this.background.toJSON():this.background.isTexture&&(i.background=this.background.toJSON(t).uuid)),this.environment&&this.environment.isTexture&&!0!==this.environment.isRenderTargetTexture&&(i.environment=this.environment.toJSON(t).uuid);else if(this.isMesh||this.isLine||this.isPoints){i.geometry=r(t.geometries,this.geometry);const e=this.geometry.parameters;if(void 0!==e&&void 0!==e.shapes){const n=e.shapes;if(Array.isArray(n))for(let e=0,i=n.length;e0){i.children=[];for(let e=0;e0){i.animations=[];for(let e=0;e0&&(n.geometries=e),i.length>0&&(n.materials=i),r.length>0&&(n.textures=r),a.length>0&&(n.images=a),o.length>0&&(n.shapes=o),l.length>0&&(n.skeletons=l),c.length>0&&(n.animations=c),h.length>0&&(n.nodes=h)}return n.object=i,n;function s(t){const e=[];for(const n in t){const i=t[n];delete i.metadata,e.push(i)}return e}}clone(t){return(new this.constructor).copy(this,t)}copy(t,e=!0){if(this.name=t.name,this.up.copy(t.up),this.position.copy(t.position),this.rotation.order=t.rotation.order,this.quaternion.copy(t.quaternion),this.scale.copy(t.scale),this.matrix.copy(t.matrix),this.matrixWorld.copy(t.matrixWorld),this.matrixAutoUpdate=t.matrixAutoUpdate,this.matrixWorldAutoUpdate=t.matrixWorldAutoUpdate,this.matrixWorldNeedsUpdate=t.matrixWorldNeedsUpdate,this.layers.mask=t.layers.mask,this.visible=t.visible,this.castShadow=t.castShadow,this.receiveShadow=t.receiveShadow,this.frustumCulled=t.frustumCulled,this.renderOrder=t.renderOrder,this.animations=t.animations.slice(),this.userData=JSON.parse(JSON.stringify(t.userData)),!0===e)for(let e=0;e0?i.multiplyScalar(1/Math.sqrt(r)):i.set(0,0,0)}static getBarycoord(t,e,n,i,r){Ur.subVectors(i,e),Dr.subVectors(n,e),Nr.subVectors(t,e);const s=Ur.dot(Ur),a=Ur.dot(Dr),o=Ur.dot(Nr),l=Dr.dot(Dr),c=Dr.dot(Nr),h=s*l-a*a;if(0===h)return r.set(0,0,0),null;const u=1/h,d=(l*o-a*c)*u,p=(s*c-a*o)*u;return r.set(1-d-p,p,d)}static containsPoint(t,e,n,i){return null!==this.getBarycoord(t,e,n,i,Or)&&(Or.x>=0&&Or.y>=0&&Or.x+Or.y<=1)}static getInterpolation(t,e,n,i,r,s,a,o){return null===this.getBarycoord(t,e,n,i,Or)?(o.x=0,o.y=0,"z"in o&&(o.z=0),"w"in o&&(o.w=0),null):(o.setScalar(0),o.addScaledVector(r,Or.x),o.addScaledVector(s,Or.y),o.addScaledVector(a,Or.z),o)}static getInterpolatedAttribute(t,e,n,i,r,s){return Gr.setScalar(0),Wr.setScalar(0),Xr.setScalar(0),Gr.fromBufferAttribute(t,e),Wr.fromBufferAttribute(t,n),Xr.fromBufferAttribute(t,i),s.setScalar(0),s.addScaledVector(Gr,r.x),s.addScaledVector(Wr,r.y),s.addScaledVector(Xr,r.z),s}static isFrontFacing(t,e,n,i){return Ur.subVectors(n,e),Dr.subVectors(t,e),Ur.cross(Dr).dot(i)<0}set(t,e,n){return this.a.copy(t),this.b.copy(e),this.c.copy(n),this}setFromPointsAndIndices(t,e,n,i){return this.a.copy(t[e]),this.b.copy(t[n]),this.c.copy(t[i]),this}setFromAttributeAndIndices(t,e,n,i){return this.a.fromBufferAttribute(t,e),this.b.fromBufferAttribute(t,n),this.c.fromBufferAttribute(t,i),this}clone(){return(new this.constructor).copy(this)}copy(t){return this.a.copy(t.a),this.b.copy(t.b),this.c.copy(t.c),this}getArea(){return Ur.subVectors(this.c,this.b),Dr.subVectors(this.a,this.b),.5*Ur.cross(Dr).length()}getMidpoint(t){return t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return jr.getNormal(this.a,this.b,this.c,t)}getPlane(t){return t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return jr.getBarycoord(t,this.a,this.b,this.c,e)}getInterpolation(t,e,n,i,r){return jr.getInterpolation(t,this.a,this.b,this.c,e,n,i,r)}containsPoint(t){return jr.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return jr.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(t){return t.intersectsTriangle(this)}closestPointToPoint(t,e){const n=this.a,i=this.b,r=this.c;let s,a;Fr.subVectors(i,n),Br.subVectors(r,n),kr.subVectors(t,n);const o=Fr.dot(kr),l=Br.dot(kr);if(o<=0&&l<=0)return e.copy(n);Vr.subVectors(t,i);const c=Fr.dot(Vr),h=Br.dot(Vr);if(c>=0&&h<=c)return e.copy(i);const u=o*h-c*l;if(u<=0&&o>=0&&c<=0)return s=o/(o-c),e.copy(n).addScaledVector(Fr,s);Hr.subVectors(t,r);const d=Fr.dot(Hr),p=Br.dot(Hr);if(p>=0&&d<=p)return e.copy(r);const m=d*l-o*p;if(m<=0&&l>=0&&p<=0)return a=l/(l-p),e.copy(n).addScaledVector(Br,a);const f=c*p-d*h;if(f<=0&&h-c>=0&&d-p>=0)return zr.subVectors(r,i),a=(h-c)/(h-c+(d-p)),e.copy(i).addScaledVector(zr,a);const g=1/(f+m+u);return s=m*g,a=u*g,e.copy(n).addScaledVector(Fr,s).addScaledVector(Br,a)}equals(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)}}const qr={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},Yr={h:0,s:0,l:0},Zr={h:0,s:0,l:0};function Jr(t,e,n){return n<0&&(n+=1),n>1&&(n-=1),n<1/6?t+6*(e-t)*n:n<.5?e:n<2/3?t+6*(e-t)*(2/3-n):t}class Kr{constructor(t,e,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(t,e,n)}set(t,e,n){if(void 0===e&&void 0===n){const e=t;e&&e.isColor?this.copy(e):"number"==typeof e?this.setHex(e):"string"==typeof e&&this.setStyle(e)}else this.setRGB(t,e,n);return this}setScalar(t){return this.r=t,this.g=t,this.b=t,this}setHex(t,e=Je){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(255&t)/255,ai.toWorkingColorSpace(this,e),this}setRGB(t,e,n,i=ai.workingColorSpace){return this.r=t,this.g=e,this.b=n,ai.toWorkingColorSpace(this,i),this}setHSL(t,e,n,i=ai.workingColorSpace){if(t=Xn(t,1),e=Wn(e,0,1),n=Wn(n,0,1),0===e)this.r=this.g=this.b=n;else{const i=n<=.5?n*(1+e):n+e-n*e,r=2*n-i;this.r=Jr(r,i,t+1/3),this.g=Jr(r,i,t),this.b=Jr(r,i,t-1/3)}return ai.toWorkingColorSpace(this,i),this}setStyle(t,e=Je){function n(e){void 0!==e&&parseFloat(e)<1&&console.warn("THREE.Color: Alpha component of "+t+" will be ignored.")}let i;if(i=/^(\w+)\(([^\)]*)\)/.exec(t)){let r;const s=i[1],a=i[2];switch(s){case"rgb":case"rgba":if(r=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(a))return n(r[4]),this.setRGB(Math.min(255,parseInt(r[1],10))/255,Math.min(255,parseInt(r[2],10))/255,Math.min(255,parseInt(r[3],10))/255,e);if(r=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(a))return n(r[4]),this.setRGB(Math.min(100,parseInt(r[1],10))/100,Math.min(100,parseInt(r[2],10))/100,Math.min(100,parseInt(r[3],10))/100,e);break;case"hsl":case"hsla":if(r=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(a))return n(r[4]),this.setHSL(parseFloat(r[1])/360,parseFloat(r[2])/100,parseFloat(r[3])/100,e);break;default:console.warn("THREE.Color: Unknown color model "+t)}}else if(i=/^\#([A-Fa-f\d]+)$/.exec(t)){const n=i[1],r=n.length;if(3===r)return this.setRGB(parseInt(n.charAt(0),16)/15,parseInt(n.charAt(1),16)/15,parseInt(n.charAt(2),16)/15,e);if(6===r)return this.setHex(parseInt(n,16),e);console.warn("THREE.Color: Invalid hex color "+t)}else if(t&&t.length>0)return this.setColorName(t,e);return this}setColorName(t,e=Je){const n=qr[t.toLowerCase()];return void 0!==n?this.setHex(n,e):console.warn("THREE.Color: Unknown color "+t),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(t){return this.r=t.r,this.g=t.g,this.b=t.b,this}copySRGBToLinear(t){return this.r=oi(t.r),this.g=oi(t.g),this.b=oi(t.b),this}copyLinearToSRGB(t){return this.r=li(t.r),this.g=li(t.g),this.b=li(t.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(t=Je){return ai.fromWorkingColorSpace($r.copy(this),t),65536*Math.round(Wn(255*$r.r,0,255))+256*Math.round(Wn(255*$r.g,0,255))+Math.round(Wn(255*$r.b,0,255))}getHexString(t=Je){return("000000"+this.getHex(t).toString(16)).slice(-6)}getHSL(t,e=ai.workingColorSpace){ai.fromWorkingColorSpace($r.copy(this),e);const n=$r.r,i=$r.g,r=$r.b,s=Math.max(n,i,r),a=Math.min(n,i,r);let o,l;const c=(a+s)/2;if(a===s)o=0,l=0;else{const t=s-a;switch(l=c<=.5?t/(s+a):t/(2-s-a),s){case n:o=(i-r)/t+(i0!=t>0&&this.version++,this._alphaTest=t}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(t){if(void 0!==t)for(const e in t){const n=t[e];if(void 0===n){console.warn(`THREE.Material: parameter '${e}' has value of undefined.`);continue}const i=this[e];void 0!==i?i&&i.isColor?i.set(n):i&&i.isVector3&&n&&n.isVector3?i.copy(n):this[e]=n:console.warn(`THREE.Material: '${e}' is not a property of THREE.${this.type}.`)}}toJSON(t){const e=void 0===t||"string"==typeof t;e&&(t={textures:{},images:{}});const n={metadata:{version:4.6,type:"Material",generator:"Material.toJSON"}};function i(t){const e=[];for(const n in t){const i=t[n];delete i.metadata,e.push(i)}return e}if(n.uuid=this.uuid,n.type=this.type,""!==this.name&&(n.name=this.name),this.color&&this.color.isColor&&(n.color=this.color.getHex()),void 0!==this.roughness&&(n.roughness=this.roughness),void 0!==this.metalness&&(n.metalness=this.metalness),void 0!==this.sheen&&(n.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(n.sheenColor=this.sheenColor.getHex()),void 0!==this.sheenRoughness&&(n.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),void 0!==this.emissiveIntensity&&1!==this.emissiveIntensity&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),void 0!==this.specularIntensity&&(n.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(n.specularColor=this.specularColor.getHex()),void 0!==this.shininess&&(n.shininess=this.shininess),void 0!==this.clearcoat&&(n.clearcoat=this.clearcoat),void 0!==this.clearcoatRoughness&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(t).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(t).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(t).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),void 0!==this.dispersion&&(n.dispersion=this.dispersion),void 0!==this.iridescence&&(n.iridescence=this.iridescence),void 0!==this.iridescenceIOR&&(n.iridescenceIOR=this.iridescenceIOR),void 0!==this.iridescenceThicknessRange&&(n.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(n.iridescenceMap=this.iridescenceMap.toJSON(t).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(t).uuid),void 0!==this.anisotropy&&(n.anisotropy=this.anisotropy),void 0!==this.anisotropyRotation&&(n.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(n.anisotropyMap=this.anisotropyMap.toJSON(t).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(t).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(t).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(t).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(t).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(t).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(t).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(t).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(t).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(t).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(t).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(t).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(t).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(t).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(t).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(t).uuid,void 0!==this.combine&&(n.combine=this.combine)),void 0!==this.envMapRotation&&(n.envMapRotation=this.envMapRotation.toArray()),void 0!==this.envMapIntensity&&(n.envMapIntensity=this.envMapIntensity),void 0!==this.reflectivity&&(n.reflectivity=this.reflectivity),void 0!==this.refractionRatio&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(t).uuid),void 0!==this.transmission&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(t).uuid),void 0!==this.thickness&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(t).uuid),void 0!==this.attenuationDistance&&this.attenuationDistance!==1/0&&(n.attenuationDistance=this.attenuationDistance),void 0!==this.attenuationColor&&(n.attenuationColor=this.attenuationColor.getHex()),void 0!==this.size&&(n.size=this.size),null!==this.shadowSide&&(n.shadowSide=this.shadowSide),void 0!==this.sizeAttenuation&&(n.sizeAttenuation=this.sizeAttenuation),1!==this.blending&&(n.blending=this.blending),0!==this.side&&(n.side=this.side),!0===this.vertexColors&&(n.vertexColors=!0),this.opacity<1&&(n.opacity=this.opacity),!0===this.transparent&&(n.transparent=!0),this.blendSrc!==C&&(n.blendSrc=this.blendSrc),this.blendDst!==I&&(n.blendDst=this.blendDst),this.blendEquation!==y&&(n.blendEquation=this.blendEquation),null!==this.blendSrcAlpha&&(n.blendSrcAlpha=this.blendSrcAlpha),null!==this.blendDstAlpha&&(n.blendDstAlpha=this.blendDstAlpha),null!==this.blendEquationAlpha&&(n.blendEquationAlpha=this.blendEquationAlpha),this.blendColor&&this.blendColor.isColor&&(n.blendColor=this.blendColor.getHex()),0!==this.blendAlpha&&(n.blendAlpha=this.blendAlpha),3!==this.depthFunc&&(n.depthFunc=this.depthFunc),!1===this.depthTest&&(n.depthTest=this.depthTest),!1===this.depthWrite&&(n.depthWrite=this.depthWrite),!1===this.colorWrite&&(n.colorWrite=this.colorWrite),255!==this.stencilWriteMask&&(n.stencilWriteMask=this.stencilWriteMask),519!==this.stencilFunc&&(n.stencilFunc=this.stencilFunc),0!==this.stencilRef&&(n.stencilRef=this.stencilRef),255!==this.stencilFuncMask&&(n.stencilFuncMask=this.stencilFuncMask),this.stencilFail!==en&&(n.stencilFail=this.stencilFail),this.stencilZFail!==en&&(n.stencilZFail=this.stencilZFail),this.stencilZPass!==en&&(n.stencilZPass=this.stencilZPass),!0===this.stencilWrite&&(n.stencilWrite=this.stencilWrite),void 0!==this.rotation&&0!==this.rotation&&(n.rotation=this.rotation),!0===this.polygonOffset&&(n.polygonOffset=!0),0!==this.polygonOffsetFactor&&(n.polygonOffsetFactor=this.polygonOffsetFactor),0!==this.polygonOffsetUnits&&(n.polygonOffsetUnits=this.polygonOffsetUnits),void 0!==this.linewidth&&1!==this.linewidth&&(n.linewidth=this.linewidth),void 0!==this.dashSize&&(n.dashSize=this.dashSize),void 0!==this.gapSize&&(n.gapSize=this.gapSize),void 0!==this.scale&&(n.scale=this.scale),!0===this.dithering&&(n.dithering=!0),this.alphaTest>0&&(n.alphaTest=this.alphaTest),!0===this.alphaHash&&(n.alphaHash=!0),!0===this.alphaToCoverage&&(n.alphaToCoverage=!0),!0===this.premultipliedAlpha&&(n.premultipliedAlpha=!0),!0===this.forceSinglePass&&(n.forceSinglePass=!0),!0===this.wireframe&&(n.wireframe=!0),this.wireframeLinewidth>1&&(n.wireframeLinewidth=this.wireframeLinewidth),"round"!==this.wireframeLinecap&&(n.wireframeLinecap=this.wireframeLinecap),"round"!==this.wireframeLinejoin&&(n.wireframeLinejoin=this.wireframeLinejoin),!0===this.flatShading&&(n.flatShading=!0),!1===this.visible&&(n.visible=!1),!1===this.toneMapped&&(n.toneMapped=!1),!1===this.fog&&(n.fog=!1),Object.keys(this.userData).length>0&&(n.userData=this.userData),e){const e=i(t.textures),r=i(t.images);e.length>0&&(n.textures=e),r.length>0&&(n.images=r)}return n}clone(){return(new this.constructor).copy(this)}copy(t){this.name=t.name,this.blending=t.blending,this.side=t.side,this.vertexColors=t.vertexColors,this.opacity=t.opacity,this.transparent=t.transparent,this.blendSrc=t.blendSrc,this.blendDst=t.blendDst,this.blendEquation=t.blendEquation,this.blendSrcAlpha=t.blendSrcAlpha,this.blendDstAlpha=t.blendDstAlpha,this.blendEquationAlpha=t.blendEquationAlpha,this.blendColor.copy(t.blendColor),this.blendAlpha=t.blendAlpha,this.depthFunc=t.depthFunc,this.depthTest=t.depthTest,this.depthWrite=t.depthWrite,this.stencilWriteMask=t.stencilWriteMask,this.stencilFunc=t.stencilFunc,this.stencilRef=t.stencilRef,this.stencilFuncMask=t.stencilFuncMask,this.stencilFail=t.stencilFail,this.stencilZFail=t.stencilZFail,this.stencilZPass=t.stencilZPass,this.stencilWrite=t.stencilWrite;const e=t.clippingPlanes;let n=null;if(null!==e){const t=e.length;n=new Array(t);for(let i=0;i!==t;++i)n[i]=e[i].clone()}return this.clippingPlanes=n,this.clipIntersection=t.clipIntersection,this.clipShadows=t.clipShadows,this.shadowSide=t.shadowSide,this.colorWrite=t.colorWrite,this.precision=t.precision,this.polygonOffset=t.polygonOffset,this.polygonOffsetFactor=t.polygonOffsetFactor,this.polygonOffsetUnits=t.polygonOffsetUnits,this.dithering=t.dithering,this.alphaTest=t.alphaTest,this.alphaHash=t.alphaHash,this.alphaToCoverage=t.alphaToCoverage,this.premultipliedAlpha=t.premultipliedAlpha,this.forceSinglePass=t.forceSinglePass,this.visible=t.visible,this.toneMapped=t.toneMapped,this.userData=JSON.parse(JSON.stringify(t.userData)),this}dispose(){this.dispatchEvent({type:"dispose"})}set needsUpdate(t){!0===t&&this.version++}onBuild(){console.warn("Material: onBuild() has been removed.")}}class es extends ts{static get type(){return"MeshBasicMaterial"}constructor(t){super(),this.isMeshBasicMaterial=!0,this.color=new Kr(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new fr,this.combine=0,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.fog=t.fog,this}}const ns=is();function is(){const t=new ArrayBuffer(4),e=new Float32Array(t),n=new Uint32Array(t),i=new Uint32Array(512),r=new Uint32Array(512);for(let t=0;t<256;++t){const e=t-127;e<-27?(i[t]=0,i[256|t]=32768,r[t]=24,r[256|t]=24):e<-14?(i[t]=1024>>-e-14,i[256|t]=1024>>-e-14|32768,r[t]=-e-1,r[256|t]=-e-1):e<=15?(i[t]=e+15<<10,i[256|t]=e+15<<10|32768,r[t]=13,r[256|t]=13):e<128?(i[t]=31744,i[256|t]=64512,r[t]=24,r[256|t]=24):(i[t]=31744,i[256|t]=64512,r[t]=13,r[256|t]=13)}const s=new Uint32Array(2048),a=new Uint32Array(64),o=new Uint32Array(64);for(let t=1;t<1024;++t){let e=t<<13,n=0;for(;!(8388608&e);)e<<=1,n-=8388608;e&=-8388609,n+=947912704,s[t]=e|n}for(let t=1024;t<2048;++t)s[t]=939524096+(t-1024<<13);for(let t=1;t<31;++t)a[t]=t<<23;a[31]=1199570944,a[32]=2147483648;for(let t=33;t<63;++t)a[t]=2147483648+(t-32<<23);a[63]=3347054592;for(let t=1;t<64;++t)32!==t&&(o[t]=1024);return{floatView:e,uint32View:n,baseTable:i,shiftTable:r,mantissaTable:s,exponentTable:a,offsetTable:o}}function rs(t){Math.abs(t)>65504&&console.warn("THREE.DataUtils.toHalfFloat(): Value out of range."),t=Wn(t,-65504,65504),ns.floatView[0]=t;const e=ns.uint32View[0],n=e>>23&511;return ns.baseTable[n]+((8388607&e)>>ns.shiftTable[n])}function ss(t){const e=t>>10;return ns.uint32View[0]=ns.mantissaTable[ns.offsetTable[e]+(1023&t)]+ns.exponentTable[e],ns.floatView[0]}const as={toHalfFloat:rs,fromHalfFloat:ss},os=new Ci,ls=new Jn;class cs{constructor(t,e,n=!1){if(Array.isArray(t))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,this.name="",this.array=t,this.itemSize=e,this.count=void 0!==t?t.length/e:0,this.normalized=n,this.usage=wn,this.updateRanges=[],this.gpuType=Lt,this.version=0}onUploadCallback(){}set needsUpdate(t){!0===t&&this.version++}setUsage(t){return this.usage=t,this}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}copy(t){return this.name=t.name,this.array=new t.array.constructor(t.array),this.itemSize=t.itemSize,this.count=t.count,this.normalized=t.normalized,this.usage=t.usage,this.gpuType=t.gpuType,this}copyAt(t,e,n){t*=this.itemSize,n*=e.itemSize;for(let i=0,r=this.itemSize;ie.count&&console.warn("THREE.BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry."),e.needsUpdate=!0}return this}computeBoundingBox(){null===this.boundingBox&&(this.boundingBox=new Li);const t=this.attributes.position,e=this.morphAttributes.position;if(t&&t.isGLBufferAttribute)return console.error("THREE.BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.",this),void this.boundingBox.set(new Ci(-1/0,-1/0,-1/0),new Ci(1/0,1/0,1/0));if(void 0!==t){if(this.boundingBox.setFromBufferAttribute(t),e)for(let t=0,n=e.length;t0&&(t.userData=this.userData),void 0!==this.parameters){const e=this.parameters;for(const n in e)void 0!==e[n]&&(t[n]=e[n]);return t}t.data={attributes:{}};const e=this.index;null!==e&&(t.data.index={type:e.array.constructor.name,array:Array.prototype.slice.call(e.array)});const n=this.attributes;for(const e in n){const i=n[e];t.data.attributes[e]=i.toJSON(t.data)}const i={};let r=!1;for(const e in this.morphAttributes){const n=this.morphAttributes[e],s=[];for(let e=0,i=n.length;e0&&(i[e]=s,r=!0)}r&&(t.data.morphAttributes=i,t.data.morphTargetsRelative=this.morphTargetsRelative);const s=this.groups;s.length>0&&(t.data.groups=JSON.parse(JSON.stringify(s)));const a=this.boundingSphere;return null!==a&&(t.data.boundingSphere={center:a.center.toArray(),radius:a.radius}),t}clone(){return(new this.constructor).copy(this)}copy(t){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;const e={};this.name=t.name;const n=t.index;null!==n&&this.setIndex(n.clone(e));const i=t.attributes;for(const t in i){const n=i[t];this.setAttribute(t,n.clone(e))}const r=t.morphAttributes;for(const t in r){const n=[],i=r[t];for(let t=0,r=i.length;t0){const n=t[e[0]];if(void 0!==n){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=n.length;t(t.far-t.near)**2)return}As.copy(r).invert(),Rs.copy(t.ray).applyMatrix4(As),null!==n.boundingBox&&!1===Rs.intersectsBox(n.boundingBox)||this._computeIntersections(t,e,Rs)}}_computeIntersections(t,e,n){let i;const r=this.geometry,s=this.material,a=r.index,o=r.attributes.position,l=r.attributes.uv,c=r.attributes.uv1,h=r.attributes.normal,u=r.groups,d=r.drawRange;if(null!==a)if(Array.isArray(s))for(let r=0,o=u.length;rn.far?null:{distance:c,point:Fs.clone(),object:t}}(t,e,n,i,Ps,Ls,Us,Os);if(h){const t=new Ci;jr.getBarycoord(Os,Ps,Ls,Us,t),r&&(h.uv=jr.getInterpolatedAttribute(r,o,l,c,t,new Jn)),s&&(h.uv1=jr.getInterpolatedAttribute(s,o,l,c,t,new Jn)),a&&(h.normal=jr.getInterpolatedAttribute(a,o,l,c,t,new Ci),h.normal.dot(i.direction)>0&&h.normal.multiplyScalar(-1));const e={a:o,b:l,c:c,normal:new Ci,materialIndex:0};jr.getNormal(Ps,Ls,Us,e.normal),h.face=e,h.barycoord=t}return h}class ks extends Es{constructor(t=1,e=1,n=1,i=1,r=1,s=1){super(),this.type="BoxGeometry",this.parameters={width:t,height:e,depth:n,widthSegments:i,heightSegments:r,depthSegments:s};const a=this;i=Math.floor(i),r=Math.floor(r),s=Math.floor(s);const o=[],l=[],c=[],h=[];let u=0,d=0;function p(t,e,n,i,r,s,p,m,f,g,v){const _=s/f,x=p/g,y=s/2,M=p/2,S=m/2,b=f+1,T=g+1;let w=0,E=0;const A=new Ci;for(let s=0;s0?1:-1,c.push(A.x,A.y,A.z),h.push(o/f),h.push(1-s/g),w+=1}}for(let t=0;t0&&(e.defines=this.defines),e.vertexShader=this.vertexShader,e.fragmentShader=this.fragmentShader,e.lights=this.lights,e.clipping=this.clipping;const n={};for(const t in this.extensions)!0===this.extensions[t]&&(n[t]=!0);return Object.keys(n).length>0&&(e.extensions=n),e}}class js extends Lr{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new sr,this.projectionMatrix=new sr,this.projectionMatrixInverse=new sr,this.coordinateSystem=On}copy(t,e){return super.copy(t,e),this.matrixWorldInverse.copy(t.matrixWorldInverse),this.projectionMatrix.copy(t.projectionMatrix),this.projectionMatrixInverse.copy(t.projectionMatrixInverse),this.coordinateSystem=t.coordinateSystem,this}getWorldDirection(t){return super.getWorldDirection(t).negate()}updateMatrixWorld(t){super.updateMatrixWorld(t),this.matrixWorldInverse.copy(this.matrixWorld).invert()}updateWorldMatrix(t,e){super.updateWorldMatrix(t,e),this.matrixWorldInverse.copy(this.matrixWorld).invert()}clone(){return(new this.constructor).copy(this)}}const qs=new Ci,Ys=new Jn,Zs=new Jn;class Js extends js{constructor(t=50,e=1,n=.1,i=2e3){super(),this.isPerspectiveCamera=!0,this.type="PerspectiveCamera",this.fov=t,this.zoom=1,this.near=n,this.far=i,this.focus=10,this.aspect=e,this.view=null,this.filmGauge=35,this.filmOffset=0,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.fov=t.fov,this.zoom=t.zoom,this.near=t.near,this.far=t.far,this.focus=t.focus,this.aspect=t.aspect,this.view=null===t.view?null:Object.assign({},t.view),this.filmGauge=t.filmGauge,this.filmOffset=t.filmOffset,this}setFocalLength(t){const e=.5*this.getFilmHeight()/t;this.fov=2*Hn*Math.atan(e),this.updateProjectionMatrix()}getFocalLength(){const t=Math.tan(.5*Vn*this.fov);return.5*this.getFilmHeight()/t}getEffectiveFOV(){return 2*Hn*Math.atan(Math.tan(.5*Vn*this.fov)/this.zoom)}getFilmWidth(){return this.filmGauge*Math.min(this.aspect,1)}getFilmHeight(){return this.filmGauge/Math.max(this.aspect,1)}getViewBounds(t,e,n){qs.set(-1,-1,.5).applyMatrix4(this.projectionMatrixInverse),e.set(qs.x,qs.y).multiplyScalar(-t/qs.z),qs.set(1,1,.5).applyMatrix4(this.projectionMatrixInverse),n.set(qs.x,qs.y).multiplyScalar(-t/qs.z)}getViewSize(t,e){return this.getViewBounds(t,Ys,Zs),e.subVectors(Zs,Ys)}setViewOffset(t,e,n,i,r,s){this.aspect=t/e,null===this.view&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=n,this.view.offsetY=i,this.view.width=r,this.view.height=s,this.updateProjectionMatrix()}clearViewOffset(){null!==this.view&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=this.near;let e=t*Math.tan(.5*Vn*this.fov)/this.zoom,n=2*e,i=this.aspect*n,r=-.5*i;const s=this.view;if(null!==this.view&&this.view.enabled){const t=s.fullWidth,a=s.fullHeight;r+=s.offsetX*i/t,e-=s.offsetY*n/a,i*=s.width/t,n*=s.height/a}const a=this.filmOffset;0!==a&&(r+=t*a/this.getFilmWidth()),this.projectionMatrix.makePerspective(r,r+i,e,e-n,t,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.fov=this.fov,e.object.zoom=this.zoom,e.object.near=this.near,e.object.far=this.far,e.object.focus=this.focus,e.object.aspect=this.aspect,null!==this.view&&(e.object.view=Object.assign({},this.view)),e.object.filmGauge=this.filmGauge,e.object.filmOffset=this.filmOffset,e}}const Ks=-90;class $s extends Lr{constructor(t,e,n){super(),this.type="CubeCamera",this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;const i=new Js(Ks,1,t,e);i.layers=this.layers,this.add(i);const r=new Js(Ks,1,t,e);r.layers=this.layers,this.add(r);const s=new Js(Ks,1,t,e);s.layers=this.layers,this.add(s);const a=new Js(Ks,1,t,e);a.layers=this.layers,this.add(a);const o=new Js(Ks,1,t,e);o.layers=this.layers,this.add(o);const l=new Js(Ks,1,t,e);l.layers=this.layers,this.add(l)}updateCoordinateSystem(){const t=this.coordinateSystem,e=this.children.concat(),[n,i,r,s,a,o]=e;for(const t of e)this.remove(t);if(t===On)n.up.set(0,1,0),n.lookAt(1,0,0),i.up.set(0,1,0),i.lookAt(-1,0,0),r.up.set(0,0,-1),r.lookAt(0,1,0),s.up.set(0,0,1),s.lookAt(0,-1,0),a.up.set(0,1,0),a.lookAt(0,0,1),o.up.set(0,1,0),o.lookAt(0,0,-1);else{if(t!==Fn)throw new Error("THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: "+t);n.up.set(0,-1,0),n.lookAt(-1,0,0),i.up.set(0,-1,0),i.lookAt(1,0,0),r.up.set(0,0,1),r.lookAt(0,1,0),s.up.set(0,0,-1),s.lookAt(0,-1,0),a.up.set(0,-1,0),a.lookAt(0,0,1),o.up.set(0,-1,0),o.lookAt(0,0,-1)}for(const t of e)this.add(t),t.updateMatrixWorld()}update(t,e){null===this.parent&&this.updateMatrixWorld();const{renderTarget:n,activeMipmapLevel:i}=this;this.coordinateSystem!==t.coordinateSystem&&(this.coordinateSystem=t.coordinateSystem,this.updateCoordinateSystem());const[r,s,a,o,l,c]=this.children,h=t.getRenderTarget(),u=t.getActiveCubeFace(),d=t.getActiveMipmapLevel(),p=t.xr.enabled;t.xr.enabled=!1;const m=n.texture.generateMipmaps;n.texture.generateMipmaps=!1,t.setRenderTarget(n,0,i),t.render(e,r),t.setRenderTarget(n,1,i),t.render(e,s),t.setRenderTarget(n,2,i),t.render(e,a),t.setRenderTarget(n,3,i),t.render(e,o),t.setRenderTarget(n,4,i),t.render(e,l),n.texture.generateMipmaps=m,t.setRenderTarget(n,5,i),t.render(e,c),t.setRenderTarget(h,u,d),t.xr.enabled=p,n.texture.needsPMREMUpdate=!0}}class Qs extends yi{constructor(t,e,n,i,r,s,a,o,l,c){super(t=void 0!==t?t:[],e=void 0!==e?e:lt,n,i,r,s,a,o,l,c),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(t){this.image=t}}class ta extends bi{constructor(t=1,e={}){super(t,t,e),this.isWebGLCubeRenderTarget=!0;const n={width:t,height:t,depth:1},i=[n,n,n,n,n,n];this.texture=new Qs(i,e.mapping,e.wrapS,e.wrapT,e.magFilter,e.minFilter,e.format,e.type,e.anisotropy,e.colorSpace),this.texture.isRenderTargetTexture=!0,this.texture.generateMipmaps=void 0!==e.generateMipmaps&&e.generateMipmaps,this.texture.minFilter=void 0!==e.minFilter?e.minFilter:Mt}fromEquirectangularTexture(t,e){this.texture.type=e.type,this.texture.colorSpace=e.colorSpace,this.texture.generateMipmaps=e.generateMipmaps,this.texture.minFilter=e.minFilter,this.texture.magFilter=e.magFilter;const n={uniforms:{tEquirect:{value:null}},vertexShader:"\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\tvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\n\t\t\t\t\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n\n\t\t\t\t}\n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvWorldDirection = transformDirection( position, modelMatrix );\n\n\t\t\t\t\t#include \n\t\t\t\t\t#include \n\n\t\t\t\t}\n\t\t\t",fragmentShader:"\n\n\t\t\t\tuniform sampler2D tEquirect;\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\t#include \n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvec3 direction = normalize( vWorldDirection );\n\n\t\t\t\t\tvec2 sampleUV = equirectUv( direction );\n\n\t\t\t\t\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\n\t\t\t\t}\n\t\t\t"},i=new ks(5,5,5),r=new Xs({name:"CubemapFromEquirect",uniforms:Vs(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:1,blending:0});r.uniforms.tEquirect.value=e;const s=new Bs(i,r),a=e.minFilter;e.minFilter===Tt&&(e.minFilter=Mt);return new $s(1,10,this).update(t,s),e.minFilter=a,s.geometry.dispose(),s.material.dispose(),this}clear(t,e,n,i){const r=t.getRenderTarget();for(let r=0;r<6;r++)t.setRenderTarget(this,r),t.clear(e,n,i);t.setRenderTarget(r)}}const ea=new Ci,na=new Ci,ia=new Kn;class ra{constructor(t=new Ci(1,0,0),e=0){this.isPlane=!0,this.normal=t,this.constant=e}set(t,e){return this.normal.copy(t),this.constant=e,this}setComponents(t,e,n,i){return this.normal.set(t,e,n),this.constant=i,this}setFromNormalAndCoplanarPoint(t,e){return this.normal.copy(t),this.constant=-e.dot(this.normal),this}setFromCoplanarPoints(t,e,n){const i=ea.subVectors(n,e).cross(na.subVectors(t,e)).normalize();return this.setFromNormalAndCoplanarPoint(i,t),this}copy(t){return this.normal.copy(t.normal),this.constant=t.constant,this}normalize(){const t=1/this.normal.length();return this.normal.multiplyScalar(t),this.constant*=t,this}negate(){return this.constant*=-1,this.normal.negate(),this}distanceToPoint(t){return this.normal.dot(t)+this.constant}distanceToSphere(t){return this.distanceToPoint(t.center)-t.radius}projectPoint(t,e){return e.copy(t).addScaledVector(this.normal,-this.distanceToPoint(t))}intersectLine(t,e){const n=t.delta(ea),i=this.normal.dot(n);if(0===i)return 0===this.distanceToPoint(t.start)?e.copy(t.start):null;const r=-(t.start.dot(this.normal)+this.constant)/i;return r<0||r>1?null:e.copy(t.start).addScaledVector(n,r)}intersectsLine(t){const e=this.distanceToPoint(t.start),n=this.distanceToPoint(t.end);return e<0&&n>0||n<0&&e>0}intersectsBox(t){return t.intersectsPlane(this)}intersectsSphere(t){return t.intersectsPlane(this)}coplanarPoint(t){return t.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(t,e){const n=e||ia.getNormalMatrix(t),i=this.coplanarPoint(ea).applyMatrix4(t),r=this.normal.applyMatrix3(n).normalize();return this.constant=-i.dot(r),this}translate(t){return this.constant-=t.dot(this.normal),this}equals(t){return t.normal.equals(this.normal)&&t.constant===this.constant}clone(){return(new this.constructor).copy(this)}}const sa=new Ji,aa=new Ci;class oa{constructor(t=new ra,e=new ra,n=new ra,i=new ra,r=new ra,s=new ra){this.planes=[t,e,n,i,r,s]}set(t,e,n,i,r,s){const a=this.planes;return a[0].copy(t),a[1].copy(e),a[2].copy(n),a[3].copy(i),a[4].copy(r),a[5].copy(s),this}copy(t){const e=this.planes;for(let n=0;n<6;n++)e[n].copy(t.planes[n]);return this}setFromProjectionMatrix(t,e=2e3){const n=this.planes,i=t.elements,r=i[0],s=i[1],a=i[2],o=i[3],l=i[4],c=i[5],h=i[6],u=i[7],d=i[8],p=i[9],m=i[10],f=i[11],g=i[12],v=i[13],_=i[14],x=i[15];if(n[0].setComponents(o-r,u-l,f-d,x-g).normalize(),n[1].setComponents(o+r,u+l,f+d,x+g).normalize(),n[2].setComponents(o+s,u+c,f+p,x+v).normalize(),n[3].setComponents(o-s,u-c,f-p,x-v).normalize(),n[4].setComponents(o-a,u-h,f-m,x-_).normalize(),e===On)n[5].setComponents(o+a,u+h,f+m,x+_).normalize();else{if(e!==Fn)throw new Error("THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: "+e);n[5].setComponents(a,h,m,_).normalize()}return this}intersectsObject(t){if(void 0!==t.boundingSphere)null===t.boundingSphere&&t.computeBoundingSphere(),sa.copy(t.boundingSphere).applyMatrix4(t.matrixWorld);else{const e=t.geometry;null===e.boundingSphere&&e.computeBoundingSphere(),sa.copy(e.boundingSphere).applyMatrix4(t.matrixWorld)}return this.intersectsSphere(sa)}intersectsSprite(t){return sa.center.set(0,0,0),sa.radius=.7071067811865476,sa.applyMatrix4(t.matrixWorld),this.intersectsSphere(sa)}intersectsSphere(t){const e=this.planes,n=t.center,i=-t.radius;for(let t=0;t<6;t++){if(e[t].distanceToPoint(n)0?t.max.x:t.min.x,aa.y=i.normal.y>0?t.max.y:t.min.y,aa.z=i.normal.z>0?t.max.z:t.min.z,i.distanceToPoint(aa)<0)return!1}return!0}containsPoint(t){const e=this.planes;for(let n=0;n<6;n++)if(e[n].distanceToPoint(t)<0)return!1;return!0}clone(){return(new this.constructor).copy(this)}}function la(){let t=null,e=!1,n=null,i=null;function r(e,s){n(e,s),i=t.requestAnimationFrame(r)}return{start:function(){!0!==e&&null!==n&&(i=t.requestAnimationFrame(r),e=!0)},stop:function(){t.cancelAnimationFrame(i),e=!1},setAnimationLoop:function(t){n=t},setContext:function(e){t=e}}}function ca(t){const e=new WeakMap;return{get:function(t){return t.isInterleavedBufferAttribute&&(t=t.data),e.get(t)},remove:function(n){n.isInterleavedBufferAttribute&&(n=n.data);const i=e.get(n);i&&(t.deleteBuffer(i.buffer),e.delete(n))},update:function(n,i){if(n.isInterleavedBufferAttribute&&(n=n.data),n.isGLBufferAttribute){const t=e.get(n);return void((!t||t.versiont.start-e.start));let e=0;for(let t=1;t 0\n\tvec4 plane;\n\t#ifdef ALPHA_TO_COVERAGE\n\t\tfloat distanceToPlane, distanceGradient;\n\t\tfloat clipOpacity = 1.0;\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\n\t\t\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\n\t\t\tclipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\n\t\t\tif ( clipOpacity == 0.0 ) discard;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\t\tfloat unionClipOpacity = 1.0;\n\t\t\t#pragma unroll_loop_start\n\t\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\t\tplane = clippingPlanes[ i ];\n\t\t\t\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\n\t\t\t\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\n\t\t\t\tunionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\n\t\t\t}\n\t\t\t#pragma unroll_loop_end\n\t\t\tclipOpacity *= 1.0 - unionClipOpacity;\n\t\t#endif\n\t\tdiffuseColor.a *= clipOpacity;\n\t\tif ( diffuseColor.a == 0.0 ) discard;\n\t#else\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tif ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\t\tbool clipped = true;\n\t\t\t#pragma unroll_loop_start\n\t\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\t\tplane = clippingPlanes[ i ];\n\t\t\t\tclipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped;\n\t\t\t}\n\t\t\t#pragma unroll_loop_end\n\t\t\tif ( clipped ) discard;\n\t\t#endif\n\t#endif\n#endif",clipping_planes_pars_fragment:"#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n\tuniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];\n#endif",clipping_planes_pars_vertex:"#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n#endif",clipping_planes_vertex:"#if NUM_CLIPPING_PLANES > 0\n\tvClipPosition = - mvPosition.xyz;\n#endif",color_fragment:"#if defined( USE_COLOR_ALPHA )\n\tdiffuseColor *= vColor;\n#elif defined( USE_COLOR )\n\tdiffuseColor.rgb *= vColor;\n#endif",color_pars_fragment:"#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR )\n\tvarying vec3 vColor;\n#endif",color_pars_vertex:"#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\n\tvarying vec3 vColor;\n#endif",color_vertex:"#if defined( USE_COLOR_ALPHA )\n\tvColor = vec4( 1.0 );\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\n\tvColor = vec3( 1.0 );\n#endif\n#ifdef USE_COLOR\n\tvColor *= color;\n#endif\n#ifdef USE_INSTANCING_COLOR\n\tvColor.xyz *= instanceColor.xyz;\n#endif\n#ifdef USE_BATCHING_COLOR\n\tvec3 batchingColor = getBatchingColor( getIndirectIndex( gl_DrawID ) );\n\tvColor.xyz *= batchingColor.xyz;\n#endif",common:"#define PI 3.141592653589793\n#define PI2 6.283185307179586\n#define PI_HALF 1.5707963267948966\n#define RECIPROCAL_PI 0.3183098861837907\n#define RECIPROCAL_PI2 0.15915494309189535\n#define EPSILON 1e-6\n#ifndef saturate\n#define saturate( a ) clamp( a, 0.0, 1.0 )\n#endif\n#define whiteComplement( a ) ( 1.0 - saturate( a ) )\nfloat pow2( const in float x ) { return x*x; }\nvec3 pow2( const in vec3 x ) { return x*x; }\nfloat pow3( const in float x ) { return x*x*x; }\nfloat pow4( const in float x ) { float x2 = x*x; return x2*x2; }\nfloat max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); }\nfloat average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); }\nhighp float rand( const in vec2 uv ) {\n\tconst highp float a = 12.9898, b = 78.233, c = 43758.5453;\n\thighp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );\n\treturn fract( sin( sn ) * c );\n}\n#ifdef HIGH_PRECISION\n\tfloat precisionSafeLength( vec3 v ) { return length( v ); }\n#else\n\tfloat precisionSafeLength( vec3 v ) {\n\t\tfloat maxComponent = max3( abs( v ) );\n\t\treturn length( v / maxComponent ) * maxComponent;\n\t}\n#endif\nstruct IncidentLight {\n\tvec3 color;\n\tvec3 direction;\n\tbool visible;\n};\nstruct ReflectedLight {\n\tvec3 directDiffuse;\n\tvec3 directSpecular;\n\tvec3 indirectDiffuse;\n\tvec3 indirectSpecular;\n};\n#ifdef USE_ALPHAHASH\n\tvarying vec3 vPosition;\n#endif\nvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n}\nvec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );\n}\nmat3 transposeMat3( const in mat3 m ) {\n\tmat3 tmp;\n\ttmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x );\n\ttmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y );\n\ttmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z );\n\treturn tmp;\n}\nbool isPerspectiveMatrix( mat4 m ) {\n\treturn m[ 2 ][ 3 ] == - 1.0;\n}\nvec2 equirectUv( in vec3 dir ) {\n\tfloat u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5;\n\tfloat v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\treturn vec2( u, v );\n}\nvec3 BRDF_Lambert( const in vec3 diffuseColor ) {\n\treturn RECIPROCAL_PI * diffuseColor;\n}\nvec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) {\n\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\n\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\n}\nfloat F_Schlick( const in float f0, const in float f90, const in float dotVH ) {\n\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\n\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\n} // validated",cube_uv_reflection_fragment:"#ifdef ENVMAP_TYPE_CUBE_UV\n\t#define cubeUV_minMipLevel 4.0\n\t#define cubeUV_minTileSize 16.0\n\tfloat getFace( vec3 direction ) {\n\t\tvec3 absDirection = abs( direction );\n\t\tfloat face = - 1.0;\n\t\tif ( absDirection.x > absDirection.z ) {\n\t\t\tif ( absDirection.x > absDirection.y )\n\t\t\t\tface = direction.x > 0.0 ? 0.0 : 3.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t} else {\n\t\t\tif ( absDirection.z > absDirection.y )\n\t\t\t\tface = direction.z > 0.0 ? 2.0 : 5.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t}\n\t\treturn face;\n\t}\n\tvec2 getUV( vec3 direction, float face ) {\n\t\tvec2 uv;\n\t\tif ( face == 0.0 ) {\n\t\t\tuv = vec2( direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 1.0 ) {\n\t\t\tuv = vec2( - direction.x, - direction.z ) / abs( direction.y );\n\t\t} else if ( face == 2.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.y ) / abs( direction.z );\n\t\t} else if ( face == 3.0 ) {\n\t\t\tuv = vec2( - direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 4.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.z ) / abs( direction.y );\n\t\t} else {\n\t\t\tuv = vec2( direction.x, direction.y ) / abs( direction.z );\n\t\t}\n\t\treturn 0.5 * ( uv + 1.0 );\n\t}\n\tvec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) {\n\t\tfloat face = getFace( direction );\n\t\tfloat filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 );\n\t\tmipInt = max( mipInt, cubeUV_minMipLevel );\n\t\tfloat faceSize = exp2( mipInt );\n\t\thighp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0;\n\t\tif ( face > 2.0 ) {\n\t\t\tuv.y += faceSize;\n\t\t\tface -= 3.0;\n\t\t}\n\t\tuv.x += face * faceSize;\n\t\tuv.x += filterInt * 3.0 * cubeUV_minTileSize;\n\t\tuv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize );\n\t\tuv.x *= CUBEUV_TEXEL_WIDTH;\n\t\tuv.y *= CUBEUV_TEXEL_HEIGHT;\n\t\t#ifdef texture2DGradEXT\n\t\t\treturn texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb;\n\t\t#else\n\t\t\treturn texture2D( envMap, uv ).rgb;\n\t\t#endif\n\t}\n\t#define cubeUV_r0 1.0\n\t#define cubeUV_m0 - 2.0\n\t#define cubeUV_r1 0.8\n\t#define cubeUV_m1 - 1.0\n\t#define cubeUV_r4 0.4\n\t#define cubeUV_m4 2.0\n\t#define cubeUV_r5 0.305\n\t#define cubeUV_m5 3.0\n\t#define cubeUV_r6 0.21\n\t#define cubeUV_m6 4.0\n\tfloat roughnessToMip( float roughness ) {\n\t\tfloat mip = 0.0;\n\t\tif ( roughness >= cubeUV_r1 ) {\n\t\t\tmip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0;\n\t\t} else if ( roughness >= cubeUV_r4 ) {\n\t\t\tmip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1;\n\t\t} else if ( roughness >= cubeUV_r5 ) {\n\t\t\tmip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4;\n\t\t} else if ( roughness >= cubeUV_r6 ) {\n\t\t\tmip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5;\n\t\t} else {\n\t\t\tmip = - 2.0 * log2( 1.16 * roughness );\t\t}\n\t\treturn mip;\n\t}\n\tvec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) {\n\t\tfloat mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP );\n\t\tfloat mipF = fract( mip );\n\t\tfloat mipInt = floor( mip );\n\t\tvec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt );\n\t\tif ( mipF == 0.0 ) {\n\t\t\treturn vec4( color0, 1.0 );\n\t\t} else {\n\t\t\tvec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 );\n\t\t\treturn vec4( mix( color0, color1, mipF ), 1.0 );\n\t\t}\n\t}\n#endif",defaultnormal_vertex:"vec3 transformedNormal = objectNormal;\n#ifdef USE_TANGENT\n\tvec3 transformedTangent = objectTangent;\n#endif\n#ifdef USE_BATCHING\n\tmat3 bm = mat3( batchingMatrix );\n\ttransformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) );\n\ttransformedNormal = bm * transformedNormal;\n\t#ifdef USE_TANGENT\n\t\ttransformedTangent = bm * transformedTangent;\n\t#endif\n#endif\n#ifdef USE_INSTANCING\n\tmat3 im = mat3( instanceMatrix );\n\ttransformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) );\n\ttransformedNormal = im * transformedNormal;\n\t#ifdef USE_TANGENT\n\t\ttransformedTangent = im * transformedTangent;\n\t#endif\n#endif\ntransformedNormal = normalMatrix * transformedNormal;\n#ifdef FLIP_SIDED\n\ttransformedNormal = - transformedNormal;\n#endif\n#ifdef USE_TANGENT\n\ttransformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz;\n\t#ifdef FLIP_SIDED\n\t\ttransformedTangent = - transformedTangent;\n\t#endif\n#endif",displacementmap_pars_vertex:"#ifdef USE_DISPLACEMENTMAP\n\tuniform sampler2D displacementMap;\n\tuniform float displacementScale;\n\tuniform float displacementBias;\n#endif",displacementmap_vertex:"#ifdef USE_DISPLACEMENTMAP\n\ttransformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias );\n#endif",emissivemap_fragment:"#ifdef USE_EMISSIVEMAP\n\tvec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv );\n\t#ifdef DECODE_VIDEO_TEXTURE_EMISSIVE\n\t\temissiveColor = sRGBTransferEOTF( emissiveColor );\n\t#endif\n\ttotalEmissiveRadiance *= emissiveColor.rgb;\n#endif",emissivemap_pars_fragment:"#ifdef USE_EMISSIVEMAP\n\tuniform sampler2D emissiveMap;\n#endif",colorspace_fragment:"gl_FragColor = linearToOutputTexel( gl_FragColor );",colorspace_pars_fragment:"vec4 LinearTransferOETF( in vec4 value ) {\n\treturn value;\n}\nvec4 sRGBTransferEOTF( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );\n}\nvec4 sRGBTransferOETF( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );\n}",envmap_fragment:"#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvec3 cameraToFrag;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToFrag = normalize( vWorldPosition - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( cameraToFrag, worldNormal );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio );\n\t\t#endif\n\t#else\n\t\tvec3 reflectVec = vReflect;\n\t#endif\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 envColor = textureCube( envMap, envMapRotation * vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) );\n\t#else\n\t\tvec4 envColor = vec4( 0.0 );\n\t#endif\n\t#ifdef ENVMAP_BLENDING_MULTIPLY\n\t\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_MIX )\n\t\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_ADD )\n\t\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\n\t#endif\n#endif",envmap_common_pars_fragment:"#ifdef USE_ENVMAP\n\tuniform float envMapIntensity;\n\tuniform float flipEnvMap;\n\tuniform mat3 envMapRotation;\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tuniform samplerCube envMap;\n\t#else\n\t\tuniform sampler2D envMap;\n\t#endif\n\t\n#endif",envmap_pars_fragment:"#ifdef USE_ENVMAP\n\tuniform float reflectivity;\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\tvarying vec3 vWorldPosition;\n\t\tuniform float refractionRatio;\n\t#else\n\t\tvarying vec3 vReflect;\n\t#endif\n#endif",envmap_pars_vertex:"#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\t\n\t\tvarying vec3 vWorldPosition;\n\t#else\n\t\tvarying vec3 vReflect;\n\t\tuniform float refractionRatio;\n\t#endif\n#endif",envmap_physical_pars_fragment:"#ifdef USE_ENVMAP\n\tvec3 getIBLIrradiance( const in vec3 normal ) {\n\t\t#ifdef ENVMAP_TYPE_CUBE_UV\n\t\t\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 );\n\t\t\treturn PI * envMapColor.rgb * envMapIntensity;\n\t\t#else\n\t\t\treturn vec3( 0.0 );\n\t\t#endif\n\t}\n\tvec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) {\n\t\t#ifdef ENVMAP_TYPE_CUBE_UV\n\t\t\tvec3 reflectVec = reflect( - viewDir, normal );\n\t\t\treflectVec = normalize( mix( reflectVec, normal, roughness * roughness) );\n\t\t\treflectVec = inverseTransformDirection( reflectVec, viewMatrix );\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness );\n\t\t\treturn envMapColor.rgb * envMapIntensity;\n\t\t#else\n\t\t\treturn vec3( 0.0 );\n\t\t#endif\n\t}\n\t#ifdef USE_ANISOTROPY\n\t\tvec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) {\n\t\t\t#ifdef ENVMAP_TYPE_CUBE_UV\n\t\t\t\tvec3 bentNormal = cross( bitangent, viewDir );\n\t\t\t\tbentNormal = normalize( cross( bentNormal, bitangent ) );\n\t\t\t\tbentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) );\n\t\t\t\treturn getIBLRadiance( viewDir, bentNormal, roughness );\n\t\t\t#else\n\t\t\t\treturn vec3( 0.0 );\n\t\t\t#endif\n\t\t}\n\t#endif\n#endif",envmap_vertex:"#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvWorldPosition = worldPosition.xyz;\n\t#else\n\t\tvec3 cameraToVertex;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToVertex = normalize( worldPosition.xyz - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvReflect = reflect( cameraToVertex, worldNormal );\n\t\t#else\n\t\t\tvReflect = refract( cameraToVertex, worldNormal, refractionRatio );\n\t\t#endif\n\t#endif\n#endif",fog_vertex:"#ifdef USE_FOG\n\tvFogDepth = - mvPosition.z;\n#endif",fog_pars_vertex:"#ifdef USE_FOG\n\tvarying float vFogDepth;\n#endif",fog_fragment:"#ifdef USE_FOG\n\t#ifdef FOG_EXP2\n\t\tfloat fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth );\n\t#else\n\t\tfloat fogFactor = smoothstep( fogNear, fogFar, vFogDepth );\n\t#endif\n\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n#endif",fog_pars_fragment:"#ifdef USE_FOG\n\tuniform vec3 fogColor;\n\tvarying float vFogDepth;\n\t#ifdef FOG_EXP2\n\t\tuniform float fogDensity;\n\t#else\n\t\tuniform float fogNear;\n\t\tuniform float fogFar;\n\t#endif\n#endif",gradientmap_pars_fragment:"#ifdef USE_GRADIENTMAP\n\tuniform sampler2D gradientMap;\n#endif\nvec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) {\n\tfloat dotNL = dot( normal, lightDirection );\n\tvec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 );\n\t#ifdef USE_GRADIENTMAP\n\t\treturn vec3( texture2D( gradientMap, coord ).r );\n\t#else\n\t\tvec2 fw = fwidth( coord ) * 0.5;\n\t\treturn mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) );\n\t#endif\n}",lightmap_pars_fragment:"#ifdef USE_LIGHTMAP\n\tuniform sampler2D lightMap;\n\tuniform float lightMapIntensity;\n#endif",lights_lambert_fragment:"LambertMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularStrength = specularStrength;",lights_lambert_pars_fragment:"varying vec3 vViewPosition;\nstruct LambertMaterial {\n\tvec3 diffuseColor;\n\tfloat specularStrength;\n};\nvoid RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Lambert\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Lambert",lights_pars_begin:"uniform bool receiveShadow;\nuniform vec3 ambientLightColor;\n#if defined( USE_LIGHT_PROBES )\n\tuniform vec3 lightProbe[ 9 ];\n#endif\nvec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {\n\tfloat x = normal.x, y = normal.y, z = normal.z;\n\tvec3 result = shCoefficients[ 0 ] * 0.886227;\n\tresult += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;\n\tresult += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;\n\tresult += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;\n\tresult += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;\n\tresult += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;\n\tresult += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );\n\tresult += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;\n\tresult += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );\n\treturn result;\n}\nvec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) {\n\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\tvec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe );\n\treturn irradiance;\n}\nvec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {\n\tvec3 irradiance = ambientLightColor;\n\treturn irradiance;\n}\nfloat getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {\n\tfloat distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );\n\tif ( cutoffDistance > 0.0 ) {\n\t\tdistanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\n\t}\n\treturn distanceFalloff;\n}\nfloat getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) {\n\treturn smoothstep( coneCosine, penumbraCosine, angleCosine );\n}\n#if NUM_DIR_LIGHTS > 0\n\tstruct DirectionalLight {\n\t\tvec3 direction;\n\t\tvec3 color;\n\t};\n\tuniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];\n\tvoid getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) {\n\t\tlight.color = directionalLight.color;\n\t\tlight.direction = directionalLight.direction;\n\t\tlight.visible = true;\n\t}\n#endif\n#if NUM_POINT_LIGHTS > 0\n\tstruct PointLight {\n\t\tvec3 position;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t};\n\tuniform PointLight pointLights[ NUM_POINT_LIGHTS ];\n\tvoid getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) {\n\t\tvec3 lVector = pointLight.position - geometryPosition;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tlight.color = pointLight.color;\n\t\tlight.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay );\n\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t}\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\tstruct SpotLight {\n\t\tvec3 position;\n\t\tvec3 direction;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t\tfloat coneCos;\n\t\tfloat penumbraCos;\n\t};\n\tuniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];\n\tvoid getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) {\n\t\tvec3 lVector = spotLight.position - geometryPosition;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat angleCos = dot( light.direction, spotLight.direction );\n\t\tfloat spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos );\n\t\tif ( spotAttenuation > 0.0 ) {\n\t\t\tfloat lightDistance = length( lVector );\n\t\t\tlight.color = spotLight.color * spotAttenuation;\n\t\t\tlight.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay );\n\t\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t\t} else {\n\t\t\tlight.color = vec3( 0.0 );\n\t\t\tlight.visible = false;\n\t\t}\n\t}\n#endif\n#if NUM_RECT_AREA_LIGHTS > 0\n\tstruct RectAreaLight {\n\t\tvec3 color;\n\t\tvec3 position;\n\t\tvec3 halfWidth;\n\t\tvec3 halfHeight;\n\t};\n\tuniform sampler2D ltc_1;\tuniform sampler2D ltc_2;\n\tuniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\tstruct HemisphereLight {\n\t\tvec3 direction;\n\t\tvec3 skyColor;\n\t\tvec3 groundColor;\n\t};\n\tuniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ];\n\tvec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) {\n\t\tfloat dotNL = dot( normal, hemiLight.direction );\n\t\tfloat hemiDiffuseWeight = 0.5 * dotNL + 0.5;\n\t\tvec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );\n\t\treturn irradiance;\n\t}\n#endif",lights_toon_fragment:"ToonMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;",lights_toon_pars_fragment:"varying vec3 vViewPosition;\nstruct ToonMaterial {\n\tvec3 diffuseColor;\n};\nvoid RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\tvec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Toon\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Toon",lights_phong_fragment:"BlinnPhongMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularColor = specular;\nmaterial.specularShininess = shininess;\nmaterial.specularStrength = specularStrength;",lights_phong_pars_fragment:"varying vec3 vViewPosition;\nstruct BlinnPhongMaterial {\n\tvec3 diffuseColor;\n\tvec3 specularColor;\n\tfloat specularShininess;\n\tfloat specularStrength;\n};\nvoid RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n\treflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength;\n}\nvoid RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_BlinnPhong\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_BlinnPhong",lights_physical_fragment:"PhysicalMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor );\nvec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) );\nfloat geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );\nmaterial.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness;\nmaterial.roughness = min( material.roughness, 1.0 );\n#ifdef IOR\n\tmaterial.ior = ior;\n\t#ifdef USE_SPECULAR\n\t\tfloat specularIntensityFactor = specularIntensity;\n\t\tvec3 specularColorFactor = specularColor;\n\t\t#ifdef USE_SPECULAR_COLORMAP\n\t\t\tspecularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb;\n\t\t#endif\n\t\t#ifdef USE_SPECULAR_INTENSITYMAP\n\t\t\tspecularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a;\n\t\t#endif\n\t\tmaterial.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor );\n\t#else\n\t\tfloat specularIntensityFactor = 1.0;\n\t\tvec3 specularColorFactor = vec3( 1.0 );\n\t\tmaterial.specularF90 = 1.0;\n\t#endif\n\tmaterial.specularColor = mix( min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor, diffuseColor.rgb, metalnessFactor );\n#else\n\tmaterial.specularColor = mix( vec3( 0.04 ), diffuseColor.rgb, metalnessFactor );\n\tmaterial.specularF90 = 1.0;\n#endif\n#ifdef USE_CLEARCOAT\n\tmaterial.clearcoat = clearcoat;\n\tmaterial.clearcoatRoughness = clearcoatRoughness;\n\tmaterial.clearcoatF0 = vec3( 0.04 );\n\tmaterial.clearcoatF90 = 1.0;\n\t#ifdef USE_CLEARCOATMAP\n\t\tmaterial.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x;\n\t#endif\n\t#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\t\tmaterial.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y;\n\t#endif\n\tmaterial.clearcoat = saturate( material.clearcoat );\tmaterial.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 );\n\tmaterial.clearcoatRoughness += geometryRoughness;\n\tmaterial.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 );\n#endif\n#ifdef USE_DISPERSION\n\tmaterial.dispersion = dispersion;\n#endif\n#ifdef USE_IRIDESCENCE\n\tmaterial.iridescence = iridescence;\n\tmaterial.iridescenceIOR = iridescenceIOR;\n\t#ifdef USE_IRIDESCENCEMAP\n\t\tmaterial.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r;\n\t#endif\n\t#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\t\tmaterial.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum;\n\t#else\n\t\tmaterial.iridescenceThickness = iridescenceThicknessMaximum;\n\t#endif\n#endif\n#ifdef USE_SHEEN\n\tmaterial.sheenColor = sheenColor;\n\t#ifdef USE_SHEEN_COLORMAP\n\t\tmaterial.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb;\n\t#endif\n\tmaterial.sheenRoughness = clamp( sheenRoughness, 0.07, 1.0 );\n\t#ifdef USE_SHEEN_ROUGHNESSMAP\n\t\tmaterial.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a;\n\t#endif\n#endif\n#ifdef USE_ANISOTROPY\n\t#ifdef USE_ANISOTROPYMAP\n\t\tmat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x );\n\t\tvec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb;\n\t\tvec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b;\n\t#else\n\t\tvec2 anisotropyV = anisotropyVector;\n\t#endif\n\tmaterial.anisotropy = length( anisotropyV );\n\tif( material.anisotropy == 0.0 ) {\n\t\tanisotropyV = vec2( 1.0, 0.0 );\n\t} else {\n\t\tanisotropyV /= material.anisotropy;\n\t\tmaterial.anisotropy = saturate( material.anisotropy );\n\t}\n\tmaterial.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) );\n\tmaterial.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y;\n\tmaterial.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y;\n#endif",lights_physical_pars_fragment:"struct PhysicalMaterial {\n\tvec3 diffuseColor;\n\tfloat roughness;\n\tvec3 specularColor;\n\tfloat specularF90;\n\tfloat dispersion;\n\t#ifdef USE_CLEARCOAT\n\t\tfloat clearcoat;\n\t\tfloat clearcoatRoughness;\n\t\tvec3 clearcoatF0;\n\t\tfloat clearcoatF90;\n\t#endif\n\t#ifdef USE_IRIDESCENCE\n\t\tfloat iridescence;\n\t\tfloat iridescenceIOR;\n\t\tfloat iridescenceThickness;\n\t\tvec3 iridescenceFresnel;\n\t\tvec3 iridescenceF0;\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tvec3 sheenColor;\n\t\tfloat sheenRoughness;\n\t#endif\n\t#ifdef IOR\n\t\tfloat ior;\n\t#endif\n\t#ifdef USE_TRANSMISSION\n\t\tfloat transmission;\n\t\tfloat transmissionAlpha;\n\t\tfloat thickness;\n\t\tfloat attenuationDistance;\n\t\tvec3 attenuationColor;\n\t#endif\n\t#ifdef USE_ANISOTROPY\n\t\tfloat anisotropy;\n\t\tfloat alphaT;\n\t\tvec3 anisotropyT;\n\t\tvec3 anisotropyB;\n\t#endif\n};\nvec3 clearcoatSpecularDirect = vec3( 0.0 );\nvec3 clearcoatSpecularIndirect = vec3( 0.0 );\nvec3 sheenSpecularDirect = vec3( 0.0 );\nvec3 sheenSpecularIndirect = vec3(0.0 );\nvec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) {\n float x = clamp( 1.0 - dotVH, 0.0, 1.0 );\n float x2 = x * x;\n float x5 = clamp( x * x2 * x2, 0.0, 0.9999 );\n return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 );\n}\nfloat V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) {\n\tfloat a2 = pow2( alpha );\n\tfloat gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\n\tfloat gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\n\treturn 0.5 / max( gv + gl, EPSILON );\n}\nfloat D_GGX( const in float alpha, const in float dotNH ) {\n\tfloat a2 = pow2( alpha );\n\tfloat denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0;\n\treturn RECIPROCAL_PI * a2 / pow2( denom );\n}\n#ifdef USE_ANISOTROPY\n\tfloat V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) {\n\t\tfloat gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) );\n\t\tfloat gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) );\n\t\tfloat v = 0.5 / ( gv + gl );\n\t\treturn saturate(v);\n\t}\n\tfloat D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) {\n\t\tfloat a2 = alphaT * alphaB;\n\t\thighp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH );\n\t\thighp float v2 = dot( v, v );\n\t\tfloat w2 = a2 / v2;\n\t\treturn RECIPROCAL_PI * a2 * pow2 ( w2 );\n\t}\n#endif\n#ifdef USE_CLEARCOAT\n\tvec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) {\n\t\tvec3 f0 = material.clearcoatF0;\n\t\tfloat f90 = material.clearcoatF90;\n\t\tfloat roughness = material.clearcoatRoughness;\n\t\tfloat alpha = pow2( roughness );\n\t\tvec3 halfDir = normalize( lightDir + viewDir );\n\t\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\t\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\t\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\t\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\n\t\tvec3 F = F_Schlick( f0, f90, dotVH );\n\t\tfloat V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );\n\t\tfloat D = D_GGX( alpha, dotNH );\n\t\treturn F * ( V * D );\n\t}\n#endif\nvec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) {\n\tvec3 f0 = material.specularColor;\n\tfloat f90 = material.specularF90;\n\tfloat roughness = material.roughness;\n\tfloat alpha = pow2( roughness );\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\n\tvec3 F = F_Schlick( f0, f90, dotVH );\n\t#ifdef USE_IRIDESCENCE\n\t\tF = mix( F, material.iridescenceFresnel, material.iridescence );\n\t#endif\n\t#ifdef USE_ANISOTROPY\n\t\tfloat dotTL = dot( material.anisotropyT, lightDir );\n\t\tfloat dotTV = dot( material.anisotropyT, viewDir );\n\t\tfloat dotTH = dot( material.anisotropyT, halfDir );\n\t\tfloat dotBL = dot( material.anisotropyB, lightDir );\n\t\tfloat dotBV = dot( material.anisotropyB, viewDir );\n\t\tfloat dotBH = dot( material.anisotropyB, halfDir );\n\t\tfloat V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL );\n\t\tfloat D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH );\n\t#else\n\t\tfloat V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );\n\t\tfloat D = D_GGX( alpha, dotNH );\n\t#endif\n\treturn F * ( V * D );\n}\nvec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) {\n\tconst float LUT_SIZE = 64.0;\n\tconst float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;\n\tconst float LUT_BIAS = 0.5 / LUT_SIZE;\n\tfloat dotNV = saturate( dot( N, V ) );\n\tvec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) );\n\tuv = uv * LUT_SCALE + LUT_BIAS;\n\treturn uv;\n}\nfloat LTC_ClippedSphereFormFactor( const in vec3 f ) {\n\tfloat l = length( f );\n\treturn max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 );\n}\nvec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) {\n\tfloat x = dot( v1, v2 );\n\tfloat y = abs( x );\n\tfloat a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y;\n\tfloat b = 3.4175940 + ( 4.1616724 + y ) * y;\n\tfloat v = a / b;\n\tfloat theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v;\n\treturn cross( v1, v2 ) * theta_sintheta;\n}\nvec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) {\n\tvec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ];\n\tvec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ];\n\tvec3 lightNormal = cross( v1, v2 );\n\tif( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 );\n\tvec3 T1, T2;\n\tT1 = normalize( V - N * dot( V, N ) );\n\tT2 = - cross( N, T1 );\n\tmat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) );\n\tvec3 coords[ 4 ];\n\tcoords[ 0 ] = mat * ( rectCoords[ 0 ] - P );\n\tcoords[ 1 ] = mat * ( rectCoords[ 1 ] - P );\n\tcoords[ 2 ] = mat * ( rectCoords[ 2 ] - P );\n\tcoords[ 3 ] = mat * ( rectCoords[ 3 ] - P );\n\tcoords[ 0 ] = normalize( coords[ 0 ] );\n\tcoords[ 1 ] = normalize( coords[ 1 ] );\n\tcoords[ 2 ] = normalize( coords[ 2 ] );\n\tcoords[ 3 ] = normalize( coords[ 3 ] );\n\tvec3 vectorFormFactor = vec3( 0.0 );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] );\n\tfloat result = LTC_ClippedSphereFormFactor( vectorFormFactor );\n\treturn vec3( result );\n}\n#if defined( USE_SHEEN )\nfloat D_Charlie( float roughness, float dotNH ) {\n\tfloat alpha = pow2( roughness );\n\tfloat invAlpha = 1.0 / alpha;\n\tfloat cos2h = dotNH * dotNH;\n\tfloat sin2h = max( 1.0 - cos2h, 0.0078125 );\n\treturn ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI );\n}\nfloat V_Neubelt( float dotNV, float dotNL ) {\n\treturn saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) );\n}\nvec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) {\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat D = D_Charlie( sheenRoughness, dotNH );\n\tfloat V = V_Neubelt( dotNV, dotNL );\n\treturn sheenColor * ( D * V );\n}\n#endif\nfloat IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat r2 = roughness * roughness;\n\tfloat a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95;\n\tfloat b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72;\n\tfloat DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) );\n\treturn saturate( DG * RECIPROCAL_PI );\n}\nvec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tconst vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 );\n\tconst vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 );\n\tvec4 r = roughness * c0 + c1;\n\tfloat a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y;\n\tvec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw;\n\treturn fab;\n}\nvec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) {\n\tvec2 fab = DFGApprox( normal, viewDir, roughness );\n\treturn specularColor * fab.x + specularF90 * fab.y;\n}\n#ifdef USE_IRIDESCENCE\nvoid computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n#else\nvoid computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n#endif\n\tvec2 fab = DFGApprox( normal, viewDir, roughness );\n\t#ifdef USE_IRIDESCENCE\n\t\tvec3 Fr = mix( specularColor, iridescenceF0, iridescence );\n\t#else\n\t\tvec3 Fr = specularColor;\n\t#endif\n\tvec3 FssEss = Fr * fab.x + specularF90 * fab.y;\n\tfloat Ess = fab.x + fab.y;\n\tfloat Ems = 1.0 - Ess;\n\tvec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619;\tvec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );\n\tsingleScatter += FssEss;\n\tmultiScatter += Fms * Ems;\n}\n#if NUM_RECT_AREA_LIGHTS > 0\n\tvoid RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\t\tvec3 normal = geometryNormal;\n\t\tvec3 viewDir = geometryViewDir;\n\t\tvec3 position = geometryPosition;\n\t\tvec3 lightPos = rectAreaLight.position;\n\t\tvec3 halfWidth = rectAreaLight.halfWidth;\n\t\tvec3 halfHeight = rectAreaLight.halfHeight;\n\t\tvec3 lightColor = rectAreaLight.color;\n\t\tfloat roughness = material.roughness;\n\t\tvec3 rectCoords[ 4 ];\n\t\trectCoords[ 0 ] = lightPos + halfWidth - halfHeight;\t\trectCoords[ 1 ] = lightPos - halfWidth - halfHeight;\n\t\trectCoords[ 2 ] = lightPos - halfWidth + halfHeight;\n\t\trectCoords[ 3 ] = lightPos + halfWidth + halfHeight;\n\t\tvec2 uv = LTC_Uv( normal, viewDir, roughness );\n\t\tvec4 t1 = texture2D( ltc_1, uv );\n\t\tvec4 t2 = texture2D( ltc_2, uv );\n\t\tmat3 mInv = mat3(\n\t\t\tvec3( t1.x, 0, t1.y ),\n\t\t\tvec3( 0, 1, 0 ),\n\t\t\tvec3( t1.z, 0, t1.w )\n\t\t);\n\t\tvec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y );\n\t\treflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords );\n\t\treflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords );\n\t}\n#endif\nvoid RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\t#ifdef USE_CLEARCOAT\n\t\tfloat dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) );\n\t\tvec3 ccIrradiance = dotNLcc * directLight.color;\n\t\tclearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material );\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tsheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness );\n\t#endif\n\treflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometryViewDir, geometryNormal, material );\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {\n\t#ifdef USE_CLEARCOAT\n\t\tclearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness );\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tsheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );\n\t#endif\n\tvec3 singleScattering = vec3( 0.0 );\n\tvec3 multiScattering = vec3( 0.0 );\n\tvec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;\n\t#ifdef USE_IRIDESCENCE\n\t\tcomputeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering );\n\t#else\n\t\tcomputeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering );\n\t#endif\n\tvec3 totalScattering = singleScattering + multiScattering;\n\tvec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) );\n\treflectedLight.indirectSpecular += radiance * singleScattering;\n\treflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance;\n\treflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance;\n}\n#define RE_Direct\t\t\t\tRE_Direct_Physical\n#define RE_Direct_RectArea\t\tRE_Direct_RectArea_Physical\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Physical\n#define RE_IndirectSpecular\t\tRE_IndirectSpecular_Physical\nfloat computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) {\n\treturn saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion );\n}",lights_fragment_begin:"\nvec3 geometryPosition = - vViewPosition;\nvec3 geometryNormal = normal;\nvec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );\nvec3 geometryClearcoatNormal = vec3( 0.0 );\n#ifdef USE_CLEARCOAT\n\tgeometryClearcoatNormal = clearcoatNormal;\n#endif\n#ifdef USE_IRIDESCENCE\n\tfloat dotNVi = saturate( dot( normal, geometryViewDir ) );\n\tif ( material.iridescenceThickness == 0.0 ) {\n\t\tmaterial.iridescence = 0.0;\n\t} else {\n\t\tmaterial.iridescence = saturate( material.iridescence );\n\t}\n\tif ( material.iridescence > 0.0 ) {\n\t\tmaterial.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor );\n\t\tmaterial.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi );\n\t}\n#endif\nIncidentLight directLight;\n#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )\n\tPointLight pointLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tpointLight = pointLights[ i ];\n\t\tgetPointLightInfo( pointLight, geometryPosition, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS )\n\t\tpointLightShadow = pointLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )\n\tSpotLight spotLight;\n\tvec4 spotColor;\n\tvec3 spotLightCoord;\n\tbool inSpotLightMap;\n\t#if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tspotLight = spotLights[ i ];\n\t\tgetSpotLightInfo( spotLight, geometryPosition, directLight );\n\t\t#if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\n\t\t#define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX\n\t\t#elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\t#define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS\n\t\t#else\n\t\t#define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\n\t\t#endif\n\t\t#if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS )\n\t\t\tspotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w;\n\t\t\tinSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) );\n\t\t\tspotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy );\n\t\t\tdirectLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color;\n\t\t#endif\n\t\t#undef SPOT_LIGHT_MAP_INDEX\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\tspotLightShadow = spotLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )\n\tDirectionalLight directionalLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tdirectionalLight = directionalLights[ i ];\n\t\tgetDirectionalLightInfo( directionalLight, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )\n\t\tdirectionalLightShadow = directionalLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )\n\tRectAreaLight rectAreaLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {\n\t\trectAreaLight = rectAreaLights[ i ];\n\t\tRE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if defined( RE_IndirectDiffuse )\n\tvec3 iblIrradiance = vec3( 0.0 );\n\tvec3 irradiance = getAmbientLightIrradiance( ambientLightColor );\n\t#if defined( USE_LIGHT_PROBES )\n\t\tirradiance += getLightProbeIrradiance( lightProbe, geometryNormal );\n\t#endif\n\t#if ( NUM_HEMI_LIGHTS > 0 )\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\t\tirradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal );\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n#endif\n#if defined( RE_IndirectSpecular )\n\tvec3 radiance = vec3( 0.0 );\n\tvec3 clearcoatRadiance = vec3( 0.0 );\n#endif",lights_fragment_maps:"#if defined( RE_IndirectDiffuse )\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel = texture2D( lightMap, vLightMapUv );\n\t\tvec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity;\n\t\tirradiance += lightMapIrradiance;\n\t#endif\n\t#if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV )\n\t\tiblIrradiance += getIBLIrradiance( geometryNormal );\n\t#endif\n#endif\n#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular )\n\t#ifdef USE_ANISOTROPY\n\t\tradiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy );\n\t#else\n\t\tradiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness );\n\t#endif\n\t#ifdef USE_CLEARCOAT\n\t\tclearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness );\n\t#endif\n#endif",lights_fragment_end:"#if defined( RE_IndirectDiffuse )\n\tRE_IndirectDiffuse( irradiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n#endif\n#if defined( RE_IndirectSpecular )\n\tRE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n#endif",logdepthbuf_fragment:"#if defined( USE_LOGDEPTHBUF )\n\tgl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;\n#endif",logdepthbuf_pars_fragment:"#if defined( USE_LOGDEPTHBUF )\n\tuniform float logDepthBufFC;\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif",logdepthbuf_pars_vertex:"#ifdef USE_LOGDEPTHBUF\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif",logdepthbuf_vertex:"#ifdef USE_LOGDEPTHBUF\n\tvFragDepth = 1.0 + gl_Position.w;\n\tvIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) );\n#endif",map_fragment:"#ifdef USE_MAP\n\tvec4 sampledDiffuseColor = texture2D( map, vMapUv );\n\t#ifdef DECODE_VIDEO_TEXTURE\n\t\tsampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor );\n\t#endif\n\tdiffuseColor *= sampledDiffuseColor;\n#endif",map_pars_fragment:"#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif",map_particle_fragment:"#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\t#if defined( USE_POINTS_UV )\n\t\tvec2 uv = vUv;\n\t#else\n\t\tvec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy;\n\t#endif\n#endif\n#ifdef USE_MAP\n\tdiffuseColor *= texture2D( map, uv );\n#endif\n#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, uv ).g;\n#endif",map_particle_pars_fragment:"#if defined( USE_POINTS_UV )\n\tvarying vec2 vUv;\n#else\n\t#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\t\tuniform mat3 uvTransform;\n\t#endif\n#endif\n#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif\n#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif",metalnessmap_fragment:"float metalnessFactor = metalness;\n#ifdef USE_METALNESSMAP\n\tvec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv );\n\tmetalnessFactor *= texelMetalness.b;\n#endif",metalnessmap_pars_fragment:"#ifdef USE_METALNESSMAP\n\tuniform sampler2D metalnessMap;\n#endif",morphinstance_vertex:"#ifdef USE_INSTANCING_MORPH\n\tfloat morphTargetInfluences[ MORPHTARGETS_COUNT ];\n\tfloat morphTargetBaseInfluence = texelFetch( morphTexture, ivec2( 0, gl_InstanceID ), 0 ).r;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\tmorphTargetInfluences[i] = texelFetch( morphTexture, ivec2( i + 1, gl_InstanceID ), 0 ).r;\n\t}\n#endif",morphcolor_vertex:"#if defined( USE_MORPHCOLORS )\n\tvColor *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\t#if defined( USE_COLOR_ALPHA )\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ];\n\t\t#elif defined( USE_COLOR )\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ];\n\t\t#endif\n\t}\n#endif",morphnormal_vertex:"#ifdef USE_MORPHNORMALS\n\tobjectNormal *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\tif ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ];\n\t}\n#endif",morphtarget_pars_vertex:"#ifdef USE_MORPHTARGETS\n\t#ifndef USE_INSTANCING_MORPH\n\t\tuniform float morphTargetBaseInfluence;\n\t\tuniform float morphTargetInfluences[ MORPHTARGETS_COUNT ];\n\t#endif\n\tuniform sampler2DArray morphTargetsTexture;\n\tuniform ivec2 morphTargetsTextureSize;\n\tvec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) {\n\t\tint texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset;\n\t\tint y = texelIndex / morphTargetsTextureSize.x;\n\t\tint x = texelIndex - y * morphTargetsTextureSize.x;\n\t\tivec3 morphUV = ivec3( x, y, morphTargetIndex );\n\t\treturn texelFetch( morphTargetsTexture, morphUV, 0 );\n\t}\n#endif",morphtarget_vertex:"#ifdef USE_MORPHTARGETS\n\ttransformed *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\tif ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ];\n\t}\n#endif",normal_fragment_begin:"float faceDirection = gl_FrontFacing ? 1.0 : - 1.0;\n#ifdef FLAT_SHADED\n\tvec3 fdx = dFdx( vViewPosition );\n\tvec3 fdy = dFdy( vViewPosition );\n\tvec3 normal = normalize( cross( fdx, fdy ) );\n#else\n\tvec3 normal = normalize( vNormal );\n\t#ifdef DOUBLE_SIDED\n\t\tnormal *= faceDirection;\n\t#endif\n#endif\n#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY )\n\t#ifdef USE_TANGENT\n\t\tmat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal );\n\t#else\n\t\tmat3 tbn = getTangentFrame( - vViewPosition, normal,\n\t\t#if defined( USE_NORMALMAP )\n\t\t\tvNormalMapUv\n\t\t#elif defined( USE_CLEARCOAT_NORMALMAP )\n\t\t\tvClearcoatNormalMapUv\n\t\t#else\n\t\t\tvUv\n\t\t#endif\n\t\t);\n\t#endif\n\t#if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED )\n\t\ttbn[0] *= faceDirection;\n\t\ttbn[1] *= faceDirection;\n\t#endif\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\t#ifdef USE_TANGENT\n\t\tmat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal );\n\t#else\n\t\tmat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv );\n\t#endif\n\t#if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED )\n\t\ttbn2[0] *= faceDirection;\n\t\ttbn2[1] *= faceDirection;\n\t#endif\n#endif\nvec3 nonPerturbedNormal = normal;",normal_fragment_maps:"#ifdef USE_NORMALMAP_OBJECTSPACE\n\tnormal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;\n\t#ifdef FLIP_SIDED\n\t\tnormal = - normal;\n\t#endif\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * faceDirection;\n\t#endif\n\tnormal = normalize( normalMatrix * normal );\n#elif defined( USE_NORMALMAP_TANGENTSPACE )\n\tvec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;\n\tmapN.xy *= normalScale;\n\tnormal = normalize( tbn * mapN );\n#elif defined( USE_BUMPMAP )\n\tnormal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection );\n#endif",normal_pars_fragment:"#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif",normal_pars_vertex:"#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif",normal_vertex:"#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n\t#ifdef USE_TANGENT\n\t\tvTangent = normalize( transformedTangent );\n\t\tvBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );\n\t#endif\n#endif",normalmap_pars_fragment:"#ifdef USE_NORMALMAP\n\tuniform sampler2D normalMap;\n\tuniform vec2 normalScale;\n#endif\n#ifdef USE_NORMALMAP_OBJECTSPACE\n\tuniform mat3 normalMatrix;\n#endif\n#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) )\n\tmat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {\n\t\tvec3 q0 = dFdx( eye_pos.xyz );\n\t\tvec3 q1 = dFdy( eye_pos.xyz );\n\t\tvec2 st0 = dFdx( uv.st );\n\t\tvec2 st1 = dFdy( uv.st );\n\t\tvec3 N = surf_norm;\n\t\tvec3 q1perp = cross( q1, N );\n\t\tvec3 q0perp = cross( N, q0 );\n\t\tvec3 T = q1perp * st0.x + q0perp * st1.x;\n\t\tvec3 B = q1perp * st0.y + q0perp * st1.y;\n\t\tfloat det = max( dot( T, T ), dot( B, B ) );\n\t\tfloat scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det );\n\t\treturn mat3( T * scale, B * scale, N );\n\t}\n#endif",clearcoat_normal_fragment_begin:"#ifdef USE_CLEARCOAT\n\tvec3 clearcoatNormal = nonPerturbedNormal;\n#endif",clearcoat_normal_fragment_maps:"#ifdef USE_CLEARCOAT_NORMALMAP\n\tvec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0;\n\tclearcoatMapN.xy *= clearcoatNormalScale;\n\tclearcoatNormal = normalize( tbn2 * clearcoatMapN );\n#endif",clearcoat_pars_fragment:"#ifdef USE_CLEARCOATMAP\n\tuniform sampler2D clearcoatMap;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tuniform sampler2D clearcoatNormalMap;\n\tuniform vec2 clearcoatNormalScale;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tuniform sampler2D clearcoatRoughnessMap;\n#endif",iridescence_pars_fragment:"#ifdef USE_IRIDESCENCEMAP\n\tuniform sampler2D iridescenceMap;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tuniform sampler2D iridescenceThicknessMap;\n#endif",opaque_fragment:"#ifdef OPAQUE\ndiffuseColor.a = 1.0;\n#endif\n#ifdef USE_TRANSMISSION\ndiffuseColor.a *= material.transmissionAlpha;\n#endif\ngl_FragColor = vec4( outgoingLight, diffuseColor.a );",packing:"vec3 packNormalToRGB( const in vec3 normal ) {\n\treturn normalize( normal ) * 0.5 + 0.5;\n}\nvec3 unpackRGBToNormal( const in vec3 rgb ) {\n\treturn 2.0 * rgb.xyz - 1.0;\n}\nconst float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;const float ShiftRight8 = 1. / 256.;\nconst float Inv255 = 1. / 255.;\nconst vec4 PackFactors = vec4( 1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0 );\nconst vec2 UnpackFactors2 = vec2( UnpackDownscale, 1.0 / PackFactors.g );\nconst vec3 UnpackFactors3 = vec3( UnpackDownscale / PackFactors.rg, 1.0 / PackFactors.b );\nconst vec4 UnpackFactors4 = vec4( UnpackDownscale / PackFactors.rgb, 1.0 / PackFactors.a );\nvec4 packDepthToRGBA( const in float v ) {\n\tif( v <= 0.0 )\n\t\treturn vec4( 0., 0., 0., 0. );\n\tif( v >= 1.0 )\n\t\treturn vec4( 1., 1., 1., 1. );\n\tfloat vuf;\n\tfloat af = modf( v * PackFactors.a, vuf );\n\tfloat bf = modf( vuf * ShiftRight8, vuf );\n\tfloat gf = modf( vuf * ShiftRight8, vuf );\n\treturn vec4( vuf * Inv255, gf * PackUpscale, bf * PackUpscale, af );\n}\nvec3 packDepthToRGB( const in float v ) {\n\tif( v <= 0.0 )\n\t\treturn vec3( 0., 0., 0. );\n\tif( v >= 1.0 )\n\t\treturn vec3( 1., 1., 1. );\n\tfloat vuf;\n\tfloat bf = modf( v * PackFactors.b, vuf );\n\tfloat gf = modf( vuf * ShiftRight8, vuf );\n\treturn vec3( vuf * Inv255, gf * PackUpscale, bf );\n}\nvec2 packDepthToRG( const in float v ) {\n\tif( v <= 0.0 )\n\t\treturn vec2( 0., 0. );\n\tif( v >= 1.0 )\n\t\treturn vec2( 1., 1. );\n\tfloat vuf;\n\tfloat gf = modf( v * 256., vuf );\n\treturn vec2( vuf * Inv255, gf );\n}\nfloat unpackRGBAToDepth( const in vec4 v ) {\n\treturn dot( v, UnpackFactors4 );\n}\nfloat unpackRGBToDepth( const in vec3 v ) {\n\treturn dot( v, UnpackFactors3 );\n}\nfloat unpackRGToDepth( const in vec2 v ) {\n\treturn v.r * UnpackFactors2.r + v.g * UnpackFactors2.g;\n}\nvec4 pack2HalfToRGBA( const in vec2 v ) {\n\tvec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) );\n\treturn vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w );\n}\nvec2 unpackRGBATo2Half( const in vec4 v ) {\n\treturn vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) );\n}\nfloat viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( viewZ + near ) / ( near - far );\n}\nfloat orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) {\n\treturn depth * ( near - far ) - near;\n}\nfloat viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ );\n}\nfloat perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) {\n\treturn ( near * far ) / ( ( far - near ) * depth - far );\n}",premultiplied_alpha_fragment:"#ifdef PREMULTIPLIED_ALPHA\n\tgl_FragColor.rgb *= gl_FragColor.a;\n#endif",project_vertex:"vec4 mvPosition = vec4( transformed, 1.0 );\n#ifdef USE_BATCHING\n\tmvPosition = batchingMatrix * mvPosition;\n#endif\n#ifdef USE_INSTANCING\n\tmvPosition = instanceMatrix * mvPosition;\n#endif\nmvPosition = modelViewMatrix * mvPosition;\ngl_Position = projectionMatrix * mvPosition;",dithering_fragment:"#ifdef DITHERING\n\tgl_FragColor.rgb = dithering( gl_FragColor.rgb );\n#endif",dithering_pars_fragment:"#ifdef DITHERING\n\tvec3 dithering( vec3 color ) {\n\t\tfloat grid_position = rand( gl_FragCoord.xy );\n\t\tvec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 );\n\t\tdither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position );\n\t\treturn color + dither_shift_RGB;\n\t}\n#endif",roughnessmap_fragment:"float roughnessFactor = roughness;\n#ifdef USE_ROUGHNESSMAP\n\tvec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv );\n\troughnessFactor *= texelRoughness.g;\n#endif",roughnessmap_pars_fragment:"#ifdef USE_ROUGHNESSMAP\n\tuniform sampler2D roughnessMap;\n#endif",shadowmap_pars_fragment:"#if NUM_SPOT_LIGHT_COORDS > 0\n\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\n#endif\n#if NUM_SPOT_LIGHT_MAPS > 0\n\tuniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ];\n#endif\n#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n\tfloat texture2DCompare( sampler2D depths, vec2 uv, float compare ) {\n\t\treturn step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) );\n\t}\n\tvec2 texture2DDistribution( sampler2D shadow, vec2 uv ) {\n\t\treturn unpackRGBATo2Half( texture2D( shadow, uv ) );\n\t}\n\tfloat VSMShadow (sampler2D shadow, vec2 uv, float compare ){\n\t\tfloat occlusion = 1.0;\n\t\tvec2 distribution = texture2DDistribution( shadow, uv );\n\t\tfloat hard_shadow = step( compare , distribution.x );\n\t\tif (hard_shadow != 1.0 ) {\n\t\t\tfloat distance = compare - distribution.x ;\n\t\t\tfloat variance = max( 0.00000, distribution.y * distribution.y );\n\t\t\tfloat softness_probability = variance / (variance + distance * distance );\t\t\tsoftness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 );\t\t\tocclusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 );\n\t\t}\n\t\treturn occlusion;\n\t}\n\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\tfloat shadow = 1.0;\n\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\tshadowCoord.z += shadowBias;\n\t\tbool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;\n\t\tbool frustumTest = inFrustum && shadowCoord.z <= 1.0;\n\t\tif ( frustumTest ) {\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx0 = - texelSize.x * shadowRadius;\n\t\t\tfloat dy0 = - texelSize.y * shadowRadius;\n\t\t\tfloat dx1 = + texelSize.x * shadowRadius;\n\t\t\tfloat dy1 = + texelSize.y * shadowRadius;\n\t\t\tfloat dx2 = dx0 / 2.0;\n\t\t\tfloat dy2 = dy0 / 2.0;\n\t\t\tfloat dx3 = dx1 / 2.0;\n\t\t\tfloat dy3 = dy1 / 2.0;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z )\n\t\t\t) * ( 1.0 / 17.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_PCF_SOFT )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx = texelSize.x;\n\t\t\tfloat dy = texelSize.y;\n\t\t\tvec2 uv = shadowCoord.xy;\n\t\t\tvec2 f = fract( uv * shadowMapSize + 0.5 );\n\t\t\tuv -= f * texelSize;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, uv, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t f.x ),\n\t\t\t\t\t mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t f.x ),\n\t\t\t\t\t f.y )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_VSM )\n\t\t\tshadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#else\n\t\t\tshadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#endif\n\t\t}\n\t\treturn mix( 1.0, shadow, shadowIntensity );\n\t}\n\tvec2 cubeToUV( vec3 v, float texelSizeY ) {\n\t\tvec3 absV = abs( v );\n\t\tfloat scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) );\n\t\tabsV *= scaleToCube;\n\t\tv *= scaleToCube * ( 1.0 - 2.0 * texelSizeY );\n\t\tvec2 planar = v.xy;\n\t\tfloat almostATexel = 1.5 * texelSizeY;\n\t\tfloat almostOne = 1.0 - almostATexel;\n\t\tif ( absV.z >= almostOne ) {\n\t\t\tif ( v.z > 0.0 )\n\t\t\t\tplanar.x = 4.0 - v.x;\n\t\t} else if ( absV.x >= almostOne ) {\n\t\t\tfloat signX = sign( v.x );\n\t\t\tplanar.x = v.z * signX + 2.0 * signX;\n\t\t} else if ( absV.y >= almostOne ) {\n\t\t\tfloat signY = sign( v.y );\n\t\t\tplanar.x = v.x + 2.0 * signY + 2.0;\n\t\t\tplanar.y = v.z * signY - 2.0;\n\t\t}\n\t\treturn vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 );\n\t}\n\tfloat getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\n\t\tfloat shadow = 1.0;\n\t\tvec3 lightToPosition = shadowCoord.xyz;\n\t\t\n\t\tfloat lightToPositionLength = length( lightToPosition );\n\t\tif ( lightToPositionLength - shadowCameraFar <= 0.0 && lightToPositionLength - shadowCameraNear >= 0.0 ) {\n\t\t\tfloat dp = ( lightToPositionLength - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear );\t\t\tdp += shadowBias;\n\t\t\tvec3 bd3D = normalize( lightToPosition );\n\t\t\tvec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) );\n\t\t\t#if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM )\n\t\t\t\tvec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y;\n\t\t\t\tshadow = (\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) +\n\t\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp )\n\t\t\t\t) * ( 1.0 / 9.0 );\n\t\t\t#else\n\t\t\t\tshadow = texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp );\n\t\t\t#endif\n\t\t}\n\t\treturn mix( 1.0, shadow, shadowIntensity );\n\t}\n#endif",shadowmap_pars_vertex:"#if NUM_SPOT_LIGHT_COORDS > 0\n\tuniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ];\n\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\n#endif\n#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n#endif",shadowmap_vertex:"#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 )\n\tvec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\tvec4 shadowWorldPosition;\n#endif\n#if defined( USE_SHADOWMAP )\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );\n\t\t\tvDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 );\n\t\t\tvPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n#endif\n#if NUM_SPOT_LIGHT_COORDS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition;\n\t\t#if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\t\tshadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias;\n\t\t#endif\n\t\tvSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n#endif",shadowmask_pars_fragment:"float getShadowMask() {\n\tfloat shadow = 1.0;\n\t#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\tdirectionalLight = directionalLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowIntensity, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) {\n\t\tspotLight = spotLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowIntensity, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\tpointLight = pointLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowIntensity, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#endif\n\treturn shadow;\n}",skinbase_vertex:"#ifdef USE_SKINNING\n\tmat4 boneMatX = getBoneMatrix( skinIndex.x );\n\tmat4 boneMatY = getBoneMatrix( skinIndex.y );\n\tmat4 boneMatZ = getBoneMatrix( skinIndex.z );\n\tmat4 boneMatW = getBoneMatrix( skinIndex.w );\n#endif",skinning_pars_vertex:"#ifdef USE_SKINNING\n\tuniform mat4 bindMatrix;\n\tuniform mat4 bindMatrixInverse;\n\tuniform highp sampler2D boneTexture;\n\tmat4 getBoneMatrix( const in float i ) {\n\t\tint size = textureSize( boneTexture, 0 ).x;\n\t\tint j = int( i ) * 4;\n\t\tint x = j % size;\n\t\tint y = j / size;\n\t\tvec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 );\n\t\tvec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 );\n\t\tvec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 );\n\t\tvec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 );\n\t\treturn mat4( v1, v2, v3, v4 );\n\t}\n#endif",skinning_vertex:"#ifdef USE_SKINNING\n\tvec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );\n\tvec4 skinned = vec4( 0.0 );\n\tskinned += boneMatX * skinVertex * skinWeight.x;\n\tskinned += boneMatY * skinVertex * skinWeight.y;\n\tskinned += boneMatZ * skinVertex * skinWeight.z;\n\tskinned += boneMatW * skinVertex * skinWeight.w;\n\ttransformed = ( bindMatrixInverse * skinned ).xyz;\n#endif",skinnormal_vertex:"#ifdef USE_SKINNING\n\tmat4 skinMatrix = mat4( 0.0 );\n\tskinMatrix += skinWeight.x * boneMatX;\n\tskinMatrix += skinWeight.y * boneMatY;\n\tskinMatrix += skinWeight.z * boneMatZ;\n\tskinMatrix += skinWeight.w * boneMatW;\n\tskinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;\n\tobjectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz;\n\t#ifdef USE_TANGENT\n\t\tobjectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz;\n\t#endif\n#endif",specularmap_fragment:"float specularStrength;\n#ifdef USE_SPECULARMAP\n\tvec4 texelSpecular = texture2D( specularMap, vSpecularMapUv );\n\tspecularStrength = texelSpecular.r;\n#else\n\tspecularStrength = 1.0;\n#endif",specularmap_pars_fragment:"#ifdef USE_SPECULARMAP\n\tuniform sampler2D specularMap;\n#endif",tonemapping_fragment:"#if defined( TONE_MAPPING )\n\tgl_FragColor.rgb = toneMapping( gl_FragColor.rgb );\n#endif",tonemapping_pars_fragment:"#ifndef saturate\n#define saturate( a ) clamp( a, 0.0, 1.0 )\n#endif\nuniform float toneMappingExposure;\nvec3 LinearToneMapping( vec3 color ) {\n\treturn saturate( toneMappingExposure * color );\n}\nvec3 ReinhardToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\treturn saturate( color / ( vec3( 1.0 ) + color ) );\n}\nvec3 CineonToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\tcolor = max( vec3( 0.0 ), color - 0.004 );\n\treturn pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );\n}\nvec3 RRTAndODTFit( vec3 v ) {\n\tvec3 a = v * ( v + 0.0245786 ) - 0.000090537;\n\tvec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;\n\treturn a / b;\n}\nvec3 ACESFilmicToneMapping( vec3 color ) {\n\tconst mat3 ACESInputMat = mat3(\n\t\tvec3( 0.59719, 0.07600, 0.02840 ),\t\tvec3( 0.35458, 0.90834, 0.13383 ),\n\t\tvec3( 0.04823, 0.01566, 0.83777 )\n\t);\n\tconst mat3 ACESOutputMat = mat3(\n\t\tvec3( 1.60475, -0.10208, -0.00327 ),\t\tvec3( -0.53108, 1.10813, -0.07276 ),\n\t\tvec3( -0.07367, -0.00605, 1.07602 )\n\t);\n\tcolor *= toneMappingExposure / 0.6;\n\tcolor = ACESInputMat * color;\n\tcolor = RRTAndODTFit( color );\n\tcolor = ACESOutputMat * color;\n\treturn saturate( color );\n}\nconst mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(\n\tvec3( 1.6605, - 0.1246, - 0.0182 ),\n\tvec3( - 0.5876, 1.1329, - 0.1006 ),\n\tvec3( - 0.0728, - 0.0083, 1.1187 )\n);\nconst mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(\n\tvec3( 0.6274, 0.0691, 0.0164 ),\n\tvec3( 0.3293, 0.9195, 0.0880 ),\n\tvec3( 0.0433, 0.0113, 0.8956 )\n);\nvec3 agxDefaultContrastApprox( vec3 x ) {\n\tvec3 x2 = x * x;\n\tvec3 x4 = x2 * x2;\n\treturn + 15.5 * x4 * x2\n\t\t- 40.14 * x4 * x\n\t\t+ 31.96 * x4\n\t\t- 6.868 * x2 * x\n\t\t+ 0.4298 * x2\n\t\t+ 0.1191 * x\n\t\t- 0.00232;\n}\nvec3 AgXToneMapping( vec3 color ) {\n\tconst mat3 AgXInsetMatrix = mat3(\n\t\tvec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ),\n\t\tvec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ),\n\t\tvec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 )\n\t);\n\tconst mat3 AgXOutsetMatrix = mat3(\n\t\tvec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ),\n\t\tvec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ),\n\t\tvec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 )\n\t);\n\tconst float AgxMinEv = - 12.47393;\tconst float AgxMaxEv = 4.026069;\n\tcolor *= toneMappingExposure;\n\tcolor = LINEAR_SRGB_TO_LINEAR_REC2020 * color;\n\tcolor = AgXInsetMatrix * color;\n\tcolor = max( color, 1e-10 );\tcolor = log2( color );\n\tcolor = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv );\n\tcolor = clamp( color, 0.0, 1.0 );\n\tcolor = agxDefaultContrastApprox( color );\n\tcolor = AgXOutsetMatrix * color;\n\tcolor = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) );\n\tcolor = LINEAR_REC2020_TO_LINEAR_SRGB * color;\n\tcolor = clamp( color, 0.0, 1.0 );\n\treturn color;\n}\nvec3 NeutralToneMapping( vec3 color ) {\n\tconst float StartCompression = 0.8 - 0.04;\n\tconst float Desaturation = 0.15;\n\tcolor *= toneMappingExposure;\n\tfloat x = min( color.r, min( color.g, color.b ) );\n\tfloat offset = x < 0.08 ? x - 6.25 * x * x : 0.04;\n\tcolor -= offset;\n\tfloat peak = max( color.r, max( color.g, color.b ) );\n\tif ( peak < StartCompression ) return color;\n\tfloat d = 1. - StartCompression;\n\tfloat newPeak = 1. - d * d / ( peak + d - StartCompression );\n\tcolor *= newPeak / peak;\n\tfloat g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. );\n\treturn mix( color, vec3( newPeak ), g );\n}\nvec3 CustomToneMapping( vec3 color ) { return color; }",transmission_fragment:"#ifdef USE_TRANSMISSION\n\tmaterial.transmission = transmission;\n\tmaterial.transmissionAlpha = 1.0;\n\tmaterial.thickness = thickness;\n\tmaterial.attenuationDistance = attenuationDistance;\n\tmaterial.attenuationColor = attenuationColor;\n\t#ifdef USE_TRANSMISSIONMAP\n\t\tmaterial.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r;\n\t#endif\n\t#ifdef USE_THICKNESSMAP\n\t\tmaterial.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g;\n\t#endif\n\tvec3 pos = vWorldPosition;\n\tvec3 v = normalize( cameraPosition - pos );\n\tvec3 n = inverseTransformDirection( normal, viewMatrix );\n\tvec4 transmitted = getIBLVolumeRefraction(\n\t\tn, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90,\n\t\tpos, modelMatrix, viewMatrix, projectionMatrix, material.dispersion, material.ior, material.thickness,\n\t\tmaterial.attenuationColor, material.attenuationDistance );\n\tmaterial.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission );\n\ttotalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission );\n#endif",transmission_pars_fragment:"#ifdef USE_TRANSMISSION\n\tuniform float transmission;\n\tuniform float thickness;\n\tuniform float attenuationDistance;\n\tuniform vec3 attenuationColor;\n\t#ifdef USE_TRANSMISSIONMAP\n\t\tuniform sampler2D transmissionMap;\n\t#endif\n\t#ifdef USE_THICKNESSMAP\n\t\tuniform sampler2D thicknessMap;\n\t#endif\n\tuniform vec2 transmissionSamplerSize;\n\tuniform sampler2D transmissionSamplerMap;\n\tuniform mat4 modelMatrix;\n\tuniform mat4 projectionMatrix;\n\tvarying vec3 vWorldPosition;\n\tfloat w0( float a ) {\n\t\treturn ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 );\n\t}\n\tfloat w1( float a ) {\n\t\treturn ( 1.0 / 6.0 ) * ( a * a * ( 3.0 * a - 6.0 ) + 4.0 );\n\t}\n\tfloat w2( float a ){\n\t\treturn ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 );\n\t}\n\tfloat w3( float a ) {\n\t\treturn ( 1.0 / 6.0 ) * ( a * a * a );\n\t}\n\tfloat g0( float a ) {\n\t\treturn w0( a ) + w1( a );\n\t}\n\tfloat g1( float a ) {\n\t\treturn w2( a ) + w3( a );\n\t}\n\tfloat h0( float a ) {\n\t\treturn - 1.0 + w1( a ) / ( w0( a ) + w1( a ) );\n\t}\n\tfloat h1( float a ) {\n\t\treturn 1.0 + w3( a ) / ( w2( a ) + w3( a ) );\n\t}\n\tvec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) {\n\t\tuv = uv * texelSize.zw + 0.5;\n\t\tvec2 iuv = floor( uv );\n\t\tvec2 fuv = fract( uv );\n\t\tfloat g0x = g0( fuv.x );\n\t\tfloat g1x = g1( fuv.x );\n\t\tfloat h0x = h0( fuv.x );\n\t\tfloat h1x = h1( fuv.x );\n\t\tfloat h0y = h0( fuv.y );\n\t\tfloat h1y = h1( fuv.y );\n\t\tvec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy;\n\t\tvec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy;\n\t\tvec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy;\n\t\tvec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy;\n\t\treturn g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) +\n\t\t\tg1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) );\n\t}\n\tvec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) {\n\t\tvec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) );\n\t\tvec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) );\n\t\tvec2 fLodSizeInv = 1.0 / fLodSize;\n\t\tvec2 cLodSizeInv = 1.0 / cLodSize;\n\t\tvec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) );\n\t\tvec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) );\n\t\treturn mix( fSample, cSample, fract( lod ) );\n\t}\n\tvec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) {\n\t\tvec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior );\n\t\tvec3 modelScale;\n\t\tmodelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) );\n\t\tmodelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) );\n\t\tmodelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) );\n\t\treturn normalize( refractionVector ) * thickness * modelScale;\n\t}\n\tfloat applyIorToRoughness( const in float roughness, const in float ior ) {\n\t\treturn roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 );\n\t}\n\tvec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) {\n\t\tfloat lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior );\n\t\treturn textureBicubic( transmissionSamplerMap, fragCoord.xy, lod );\n\t}\n\tvec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) {\n\t\tif ( isinf( attenuationDistance ) ) {\n\t\t\treturn vec3( 1.0 );\n\t\t} else {\n\t\t\tvec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance;\n\t\t\tvec3 transmittance = exp( - attenuationCoefficient * transmissionDistance );\t\t\treturn transmittance;\n\t\t}\n\t}\n\tvec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor,\n\t\tconst in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix,\n\t\tconst in mat4 viewMatrix, const in mat4 projMatrix, const in float dispersion, const in float ior, const in float thickness,\n\t\tconst in vec3 attenuationColor, const in float attenuationDistance ) {\n\t\tvec4 transmittedLight;\n\t\tvec3 transmittance;\n\t\t#ifdef USE_DISPERSION\n\t\t\tfloat halfSpread = ( ior - 1.0 ) * 0.025 * dispersion;\n\t\t\tvec3 iors = vec3( ior - halfSpread, ior, ior + halfSpread );\n\t\t\tfor ( int i = 0; i < 3; i ++ ) {\n\t\t\t\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, iors[ i ], modelMatrix );\n\t\t\t\tvec3 refractedRayExit = position + transmissionRay;\n\t\t\n\t\t\t\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\n\t\t\t\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\n\t\t\t\trefractionCoords += 1.0;\n\t\t\t\trefractionCoords /= 2.0;\n\t\t\n\t\t\t\tvec4 transmissionSample = getTransmissionSample( refractionCoords, roughness, iors[ i ] );\n\t\t\t\ttransmittedLight[ i ] = transmissionSample[ i ];\n\t\t\t\ttransmittedLight.a += transmissionSample.a;\n\t\t\t\ttransmittance[ i ] = diffuseColor[ i ] * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance )[ i ];\n\t\t\t}\n\t\t\ttransmittedLight.a /= 3.0;\n\t\t\n\t\t#else\n\t\t\n\t\t\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix );\n\t\t\tvec3 refractedRayExit = position + transmissionRay;\n\t\t\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\n\t\t\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\n\t\t\trefractionCoords += 1.0;\n\t\t\trefractionCoords /= 2.0;\n\t\t\ttransmittedLight = getTransmissionSample( refractionCoords, roughness, ior );\n\t\t\ttransmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance );\n\t\t\n\t\t#endif\n\t\tvec3 attenuatedColor = transmittance * transmittedLight.rgb;\n\t\tvec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness );\n\t\tfloat transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0;\n\t\treturn vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor );\n\t}\n#endif",uv_pars_fragment:"#if defined( USE_UV ) || defined( USE_ANISOTROPY )\n\tvarying vec2 vUv;\n#endif\n#ifdef USE_MAP\n\tvarying vec2 vMapUv;\n#endif\n#ifdef USE_ALPHAMAP\n\tvarying vec2 vAlphaMapUv;\n#endif\n#ifdef USE_LIGHTMAP\n\tvarying vec2 vLightMapUv;\n#endif\n#ifdef USE_AOMAP\n\tvarying vec2 vAoMapUv;\n#endif\n#ifdef USE_BUMPMAP\n\tvarying vec2 vBumpMapUv;\n#endif\n#ifdef USE_NORMALMAP\n\tvarying vec2 vNormalMapUv;\n#endif\n#ifdef USE_EMISSIVEMAP\n\tvarying vec2 vEmissiveMapUv;\n#endif\n#ifdef USE_METALNESSMAP\n\tvarying vec2 vMetalnessMapUv;\n#endif\n#ifdef USE_ROUGHNESSMAP\n\tvarying vec2 vRoughnessMapUv;\n#endif\n#ifdef USE_ANISOTROPYMAP\n\tvarying vec2 vAnisotropyMapUv;\n#endif\n#ifdef USE_CLEARCOATMAP\n\tvarying vec2 vClearcoatMapUv;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tvarying vec2 vClearcoatNormalMapUv;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tvarying vec2 vClearcoatRoughnessMapUv;\n#endif\n#ifdef USE_IRIDESCENCEMAP\n\tvarying vec2 vIridescenceMapUv;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tvarying vec2 vIridescenceThicknessMapUv;\n#endif\n#ifdef USE_SHEEN_COLORMAP\n\tvarying vec2 vSheenColorMapUv;\n#endif\n#ifdef USE_SHEEN_ROUGHNESSMAP\n\tvarying vec2 vSheenRoughnessMapUv;\n#endif\n#ifdef USE_SPECULARMAP\n\tvarying vec2 vSpecularMapUv;\n#endif\n#ifdef USE_SPECULAR_COLORMAP\n\tvarying vec2 vSpecularColorMapUv;\n#endif\n#ifdef USE_SPECULAR_INTENSITYMAP\n\tvarying vec2 vSpecularIntensityMapUv;\n#endif\n#ifdef USE_TRANSMISSIONMAP\n\tuniform mat3 transmissionMapTransform;\n\tvarying vec2 vTransmissionMapUv;\n#endif\n#ifdef USE_THICKNESSMAP\n\tuniform mat3 thicknessMapTransform;\n\tvarying vec2 vThicknessMapUv;\n#endif",uv_pars_vertex:"#if defined( USE_UV ) || defined( USE_ANISOTROPY )\n\tvarying vec2 vUv;\n#endif\n#ifdef USE_MAP\n\tuniform mat3 mapTransform;\n\tvarying vec2 vMapUv;\n#endif\n#ifdef USE_ALPHAMAP\n\tuniform mat3 alphaMapTransform;\n\tvarying vec2 vAlphaMapUv;\n#endif\n#ifdef USE_LIGHTMAP\n\tuniform mat3 lightMapTransform;\n\tvarying vec2 vLightMapUv;\n#endif\n#ifdef USE_AOMAP\n\tuniform mat3 aoMapTransform;\n\tvarying vec2 vAoMapUv;\n#endif\n#ifdef USE_BUMPMAP\n\tuniform mat3 bumpMapTransform;\n\tvarying vec2 vBumpMapUv;\n#endif\n#ifdef USE_NORMALMAP\n\tuniform mat3 normalMapTransform;\n\tvarying vec2 vNormalMapUv;\n#endif\n#ifdef USE_DISPLACEMENTMAP\n\tuniform mat3 displacementMapTransform;\n\tvarying vec2 vDisplacementMapUv;\n#endif\n#ifdef USE_EMISSIVEMAP\n\tuniform mat3 emissiveMapTransform;\n\tvarying vec2 vEmissiveMapUv;\n#endif\n#ifdef USE_METALNESSMAP\n\tuniform mat3 metalnessMapTransform;\n\tvarying vec2 vMetalnessMapUv;\n#endif\n#ifdef USE_ROUGHNESSMAP\n\tuniform mat3 roughnessMapTransform;\n\tvarying vec2 vRoughnessMapUv;\n#endif\n#ifdef USE_ANISOTROPYMAP\n\tuniform mat3 anisotropyMapTransform;\n\tvarying vec2 vAnisotropyMapUv;\n#endif\n#ifdef USE_CLEARCOATMAP\n\tuniform mat3 clearcoatMapTransform;\n\tvarying vec2 vClearcoatMapUv;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tuniform mat3 clearcoatNormalMapTransform;\n\tvarying vec2 vClearcoatNormalMapUv;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tuniform mat3 clearcoatRoughnessMapTransform;\n\tvarying vec2 vClearcoatRoughnessMapUv;\n#endif\n#ifdef USE_SHEEN_COLORMAP\n\tuniform mat3 sheenColorMapTransform;\n\tvarying vec2 vSheenColorMapUv;\n#endif\n#ifdef USE_SHEEN_ROUGHNESSMAP\n\tuniform mat3 sheenRoughnessMapTransform;\n\tvarying vec2 vSheenRoughnessMapUv;\n#endif\n#ifdef USE_IRIDESCENCEMAP\n\tuniform mat3 iridescenceMapTransform;\n\tvarying vec2 vIridescenceMapUv;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tuniform mat3 iridescenceThicknessMapTransform;\n\tvarying vec2 vIridescenceThicknessMapUv;\n#endif\n#ifdef USE_SPECULARMAP\n\tuniform mat3 specularMapTransform;\n\tvarying vec2 vSpecularMapUv;\n#endif\n#ifdef USE_SPECULAR_COLORMAP\n\tuniform mat3 specularColorMapTransform;\n\tvarying vec2 vSpecularColorMapUv;\n#endif\n#ifdef USE_SPECULAR_INTENSITYMAP\n\tuniform mat3 specularIntensityMapTransform;\n\tvarying vec2 vSpecularIntensityMapUv;\n#endif\n#ifdef USE_TRANSMISSIONMAP\n\tuniform mat3 transmissionMapTransform;\n\tvarying vec2 vTransmissionMapUv;\n#endif\n#ifdef USE_THICKNESSMAP\n\tuniform mat3 thicknessMapTransform;\n\tvarying vec2 vThicknessMapUv;\n#endif",uv_vertex:"#if defined( USE_UV ) || defined( USE_ANISOTROPY )\n\tvUv = vec3( uv, 1 ).xy;\n#endif\n#ifdef USE_MAP\n\tvMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_ALPHAMAP\n\tvAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_LIGHTMAP\n\tvLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_AOMAP\n\tvAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_BUMPMAP\n\tvBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_NORMALMAP\n\tvNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_DISPLACEMENTMAP\n\tvDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_EMISSIVEMAP\n\tvEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_METALNESSMAP\n\tvMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_ROUGHNESSMAP\n\tvRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_ANISOTROPYMAP\n\tvAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_CLEARCOATMAP\n\tvClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tvClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tvClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_IRIDESCENCEMAP\n\tvIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tvIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SHEEN_COLORMAP\n\tvSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SHEEN_ROUGHNESSMAP\n\tvSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SPECULARMAP\n\tvSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SPECULAR_COLORMAP\n\tvSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SPECULAR_INTENSITYMAP\n\tvSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_TRANSMISSIONMAP\n\tvTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_THICKNESSMAP\n\tvThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy;\n#endif",worldpos_vertex:"#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0\n\tvec4 worldPosition = vec4( transformed, 1.0 );\n\t#ifdef USE_BATCHING\n\t\tworldPosition = batchingMatrix * worldPosition;\n\t#endif\n\t#ifdef USE_INSTANCING\n\t\tworldPosition = instanceMatrix * worldPosition;\n\t#endif\n\tworldPosition = modelMatrix * worldPosition;\n#endif",background_vert:"varying vec2 vUv;\nuniform mat3 uvTransform;\nvoid main() {\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n\tgl_Position = vec4( position.xy, 1.0, 1.0 );\n}",background_frag:"uniform sampler2D t2D;\nuniform float backgroundIntensity;\nvarying vec2 vUv;\nvoid main() {\n\tvec4 texColor = texture2D( t2D, vUv );\n\t#ifdef DECODE_VIDEO_TEXTURE\n\t\ttexColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w );\n\t#endif\n\ttexColor.rgb *= backgroundIntensity;\n\tgl_FragColor = texColor;\n\t#include \n\t#include \n}",backgroundCube_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n\tgl_Position.z = gl_Position.w;\n}",backgroundCube_frag:"#ifdef ENVMAP_TYPE_CUBE\n\tuniform samplerCube envMap;\n#elif defined( ENVMAP_TYPE_CUBE_UV )\n\tuniform sampler2D envMap;\n#endif\nuniform float flipEnvMap;\nuniform float backgroundBlurriness;\nuniform float backgroundIntensity;\nuniform mat3 backgroundRotation;\nvarying vec3 vWorldDirection;\n#include \nvoid main() {\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 texColor = textureCube( envMap, backgroundRotation * vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) );\n\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\tvec4 texColor = textureCubeUV( envMap, backgroundRotation * vWorldDirection, backgroundBlurriness );\n\t#else\n\t\tvec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t#endif\n\ttexColor.rgb *= backgroundIntensity;\n\tgl_FragColor = texColor;\n\t#include \n\t#include \n}",cube_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n\tgl_Position.z = gl_Position.w;\n}",cube_frag:"uniform samplerCube tCube;\nuniform float tFlip;\nuniform float opacity;\nvarying vec3 vWorldDirection;\nvoid main() {\n\tvec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) );\n\tgl_FragColor = texColor;\n\tgl_FragColor.a *= opacity;\n\t#include \n\t#include \n}",depth_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvHighPrecisionZW = gl_Position.zw;\n}",depth_frag:"#if DEPTH_PACKING == 3200\n\tuniform float opacity;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include \n\t#if DEPTH_PACKING == 3200\n\t\tdiffuseColor.a = opacity;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tfloat fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;\n\t#if DEPTH_PACKING == 3200\n\t\tgl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );\n\t#elif DEPTH_PACKING == 3201\n\t\tgl_FragColor = packDepthToRGBA( fragCoordZ );\n\t#elif DEPTH_PACKING == 3202\n\t\tgl_FragColor = vec4( packDepthToRGB( fragCoordZ ), 1.0 );\n\t#elif DEPTH_PACKING == 3203\n\t\tgl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 );\n\t#endif\n}",distanceRGBA_vert:"#define DISTANCE\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvWorldPosition = worldPosition.xyz;\n}",distanceRGBA_frag:"#define DISTANCE\nuniform vec3 referencePosition;\nuniform float nearDistance;\nuniform float farDistance;\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main () {\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tfloat dist = length( vWorldPosition - referencePosition );\n\tdist = ( dist - nearDistance ) / ( farDistance - nearDistance );\n\tdist = saturate( dist );\n\tgl_FragColor = packDepthToRGBA( dist );\n}",equirect_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n}",equirect_frag:"uniform sampler2D tEquirect;\nvarying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvec3 direction = normalize( vWorldDirection );\n\tvec2 sampleUV = equirectUv( direction );\n\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\t#include \n\t#include \n}",linedashed_vert:"uniform float scale;\nattribute float lineDistance;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvLineDistance = scale * lineDistance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",linedashed_frag:"uniform vec3 diffuse;\nuniform float opacity;\nuniform float dashSize;\nuniform float totalSize;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tif ( mod( vLineDistance, totalSize ) > dashSize ) {\n\t\tdiscard;\n\t}\n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshbasic_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#if defined ( USE_ENVMAP ) || defined ( USE_SKINNING )\n\t\t#include \n\t\t#include \n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshbasic_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel = texture2D( lightMap, vLightMapUv );\n\t\treflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI;\n\t#else\n\t\treflectedLight.indirectDiffuse += vec3( 1.0 );\n\t#endif\n\t#include \n\treflectedLight.indirectDiffuse *= diffuseColor.rgb;\n\tvec3 outgoingLight = reflectedLight.indirectDiffuse;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshlambert_vert:"#define LAMBERT\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}",meshlambert_frag:"#define LAMBERT\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshmatcap_vert:"#define MATCAP\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n}",meshmatcap_frag:"#define MATCAP\nuniform vec3 diffuse;\nuniform float opacity;\nuniform sampler2D matcap;\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 viewDir = normalize( vViewPosition );\n\tvec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) );\n\tvec3 y = cross( viewDir, x );\n\tvec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5;\n\t#ifdef USE_MATCAP\n\t\tvec4 matcapColor = texture2D( matcap, uv );\n\t#else\n\t\tvec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 );\n\t#endif\n\tvec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshnormal_vert:"#define NORMAL\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\n\tvarying vec3 vViewPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\n\tvViewPosition = - mvPosition.xyz;\n#endif\n}",meshnormal_frag:"#define NORMAL\nuniform float opacity;\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\n\tvarying vec3 vViewPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\tgl_FragColor = vec4( packNormalToRGB( normal ), diffuseColor.a );\n\t#ifdef OPAQUE\n\t\tgl_FragColor.a = 1.0;\n\t#endif\n}",meshphong_vert:"#define PHONG\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}",meshphong_frag:"#define PHONG\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform vec3 specular;\nuniform float shininess;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshphysical_vert:"#define STANDARD\nvarying vec3 vViewPosition;\n#ifdef USE_TRANSMISSION\n\tvarying vec3 vWorldPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n#ifdef USE_TRANSMISSION\n\tvWorldPosition = worldPosition.xyz;\n#endif\n}",meshphysical_frag:"#define STANDARD\n#ifdef PHYSICAL\n\t#define IOR\n\t#define USE_SPECULAR\n#endif\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\n#ifdef IOR\n\tuniform float ior;\n#endif\n#ifdef USE_SPECULAR\n\tuniform float specularIntensity;\n\tuniform vec3 specularColor;\n\t#ifdef USE_SPECULAR_COLORMAP\n\t\tuniform sampler2D specularColorMap;\n\t#endif\n\t#ifdef USE_SPECULAR_INTENSITYMAP\n\t\tuniform sampler2D specularIntensityMap;\n\t#endif\n#endif\n#ifdef USE_CLEARCOAT\n\tuniform float clearcoat;\n\tuniform float clearcoatRoughness;\n#endif\n#ifdef USE_DISPERSION\n\tuniform float dispersion;\n#endif\n#ifdef USE_IRIDESCENCE\n\tuniform float iridescence;\n\tuniform float iridescenceIOR;\n\tuniform float iridescenceThicknessMinimum;\n\tuniform float iridescenceThicknessMaximum;\n#endif\n#ifdef USE_SHEEN\n\tuniform vec3 sheenColor;\n\tuniform float sheenRoughness;\n\t#ifdef USE_SHEEN_COLORMAP\n\t\tuniform sampler2D sheenColorMap;\n\t#endif\n\t#ifdef USE_SHEEN_ROUGHNESSMAP\n\t\tuniform sampler2D sheenRoughnessMap;\n\t#endif\n#endif\n#ifdef USE_ANISOTROPY\n\tuniform vec2 anisotropyVector;\n\t#ifdef USE_ANISOTROPYMAP\n\t\tuniform sampler2D anisotropyMap;\n\t#endif\n#endif\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;\n\tvec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;\n\t#include \n\tvec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;\n\t#ifdef USE_SHEEN\n\t\tfloat sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor );\n\t\toutgoingLight = outgoingLight * sheenEnergyComp + sheenSpecularDirect + sheenSpecularIndirect;\n\t#endif\n\t#ifdef USE_CLEARCOAT\n\t\tfloat dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) );\n\t\tvec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc );\n\t\toutgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + ( clearcoatSpecularDirect + clearcoatSpecularIndirect ) * material.clearcoat;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshtoon_vert:"#define TOON\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n}",meshtoon_frag:"#define TOON\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",points_vert:"uniform float size;\nuniform float scale;\n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef USE_POINTS_UV\n\tvarying vec2 vUv;\n\tuniform mat3 uvTransform;\n#endif\nvoid main() {\n\t#ifdef USE_POINTS_UV\n\t\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tgl_PointSize = size;\n\t#ifdef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n}",points_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",shadow_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",shadow_frag:"uniform vec3 color;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tgl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\n\t#include \n\t#include \n\t#include \n}",sprite_vert:"uniform float rotation;\nuniform vec2 center;\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 mvPosition = modelViewMatrix[ 3 ];\n\tvec2 scale = vec2( length( modelMatrix[ 0 ].xyz ), length( modelMatrix[ 1 ].xyz ) );\n\t#ifndef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) scale *= - mvPosition.z;\n\t#endif\n\tvec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;\n\tvec2 rotatedPosition;\n\trotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;\n\trotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;\n\tmvPosition.xy += rotatedPosition;\n\tgl_Position = projectionMatrix * mvPosition;\n\t#include \n\t#include \n\t#include \n}",sprite_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n}"},da={common:{diffuse:{value:new Kr(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new Kn},alphaMap:{value:null},alphaMapTransform:{value:new Kn},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new Kn}},envmap:{envMap:{value:null},envMapRotation:{value:new Kn},flipEnvMap:{value:-1},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new Kn}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new Kn}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new Kn},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new Kn},normalScale:{value:new Jn(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new Kn},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new Kn}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new Kn}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new Kn}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new Kr(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMap:{value:[]},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotShadowMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMap:{value:[]},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null}},points:{diffuse:{value:new Kr(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new Kn},alphaTest:{value:0},uvTransform:{value:new Kn}},sprite:{diffuse:{value:new Kr(16777215)},opacity:{value:1},center:{value:new Jn(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new Kn},alphaMap:{value:null},alphaMapTransform:{value:new Kn},alphaTest:{value:0}}},pa={basic:{uniforms:Hs([da.common,da.specularmap,da.envmap,da.aomap,da.lightmap,da.fog]),vertexShader:ua.meshbasic_vert,fragmentShader:ua.meshbasic_frag},lambert:{uniforms:Hs([da.common,da.specularmap,da.envmap,da.aomap,da.lightmap,da.emissivemap,da.bumpmap,da.normalmap,da.displacementmap,da.fog,da.lights,{emissive:{value:new Kr(0)}}]),vertexShader:ua.meshlambert_vert,fragmentShader:ua.meshlambert_frag},phong:{uniforms:Hs([da.common,da.specularmap,da.envmap,da.aomap,da.lightmap,da.emissivemap,da.bumpmap,da.normalmap,da.displacementmap,da.fog,da.lights,{emissive:{value:new Kr(0)},specular:{value:new Kr(1118481)},shininess:{value:30}}]),vertexShader:ua.meshphong_vert,fragmentShader:ua.meshphong_frag},standard:{uniforms:Hs([da.common,da.envmap,da.aomap,da.lightmap,da.emissivemap,da.bumpmap,da.normalmap,da.displacementmap,da.roughnessmap,da.metalnessmap,da.fog,da.lights,{emissive:{value:new Kr(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:ua.meshphysical_vert,fragmentShader:ua.meshphysical_frag},toon:{uniforms:Hs([da.common,da.aomap,da.lightmap,da.emissivemap,da.bumpmap,da.normalmap,da.displacementmap,da.gradientmap,da.fog,da.lights,{emissive:{value:new Kr(0)}}]),vertexShader:ua.meshtoon_vert,fragmentShader:ua.meshtoon_frag},matcap:{uniforms:Hs([da.common,da.bumpmap,da.normalmap,da.displacementmap,da.fog,{matcap:{value:null}}]),vertexShader:ua.meshmatcap_vert,fragmentShader:ua.meshmatcap_frag},points:{uniforms:Hs([da.points,da.fog]),vertexShader:ua.points_vert,fragmentShader:ua.points_frag},dashed:{uniforms:Hs([da.common,da.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:ua.linedashed_vert,fragmentShader:ua.linedashed_frag},depth:{uniforms:Hs([da.common,da.displacementmap]),vertexShader:ua.depth_vert,fragmentShader:ua.depth_frag},normal:{uniforms:Hs([da.common,da.bumpmap,da.normalmap,da.displacementmap,{opacity:{value:1}}]),vertexShader:ua.meshnormal_vert,fragmentShader:ua.meshnormal_frag},sprite:{uniforms:Hs([da.sprite,da.fog]),vertexShader:ua.sprite_vert,fragmentShader:ua.sprite_frag},background:{uniforms:{uvTransform:{value:new Kn},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:ua.background_vert,fragmentShader:ua.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new Kn}},vertexShader:ua.backgroundCube_vert,fragmentShader:ua.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:ua.cube_vert,fragmentShader:ua.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:ua.equirect_vert,fragmentShader:ua.equirect_frag},distanceRGBA:{uniforms:Hs([da.common,da.displacementmap,{referencePosition:{value:new Ci},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:ua.distanceRGBA_vert,fragmentShader:ua.distanceRGBA_frag},shadow:{uniforms:Hs([da.lights,da.fog,{color:{value:new Kr(0)},opacity:{value:1}}]),vertexShader:ua.shadow_vert,fragmentShader:ua.shadow_frag}};pa.physical={uniforms:Hs([pa.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new Kn},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new Kn},clearcoatNormalScale:{value:new Jn(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new Kn},dispersion:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new Kn},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new Kn},sheen:{value:0},sheenColor:{value:new Kr(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new Kn},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new Kn},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new Kn},transmissionSamplerSize:{value:new Jn},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new Kn},attenuationDistance:{value:0},attenuationColor:{value:new Kr(0)},specularColor:{value:new Kr(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new Kn},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new Kn},anisotropyVector:{value:new Jn},anisotropyMap:{value:null},anisotropyMapTransform:{value:new Kn}}]),vertexShader:ua.meshphysical_vert,fragmentShader:ua.meshphysical_frag};const ma={r:0,b:0,g:0},fa=new fr,ga=new sr;function va(t,e,n,i,r,s,a){const o=new Kr(0);let l,c,h=!0===s?0:1,u=null,d=0,p=null;function m(t){let i=!0===t.isScene?t.background:null;if(i&&i.isTexture){i=(t.backgroundBlurriness>0?n:e).get(i)}return i}function f(e,n){e.getRGB(ma,Gs(t)),i.buffers.color.setClear(ma.r,ma.g,ma.b,n,a)}return{getClearColor:function(){return o},setClearColor:function(t,e=1){o.set(t),h=e,f(o,h)},getClearAlpha:function(){return h},setClearAlpha:function(t){h=t,f(o,h)},render:function(e){let n=!1;const r=m(e);null===r?f(o,h):r&&r.isColor&&(f(r,1),n=!0);const s=t.xr.getEnvironmentBlendMode();"additive"===s?i.buffers.color.setClear(0,0,0,1,a):"alpha-blend"===s&&i.buffers.color.setClear(0,0,0,0,a),(t.autoClear||n)&&(i.buffers.depth.setTest(!0),i.buffers.depth.setMask(!0),i.buffers.color.setMask(!0),t.clear(t.autoClearColor,t.autoClearDepth,t.autoClearStencil))},addToRenderList:function(e,n){const i=m(n);i&&(i.isCubeTexture||i.mapping===dt)?(void 0===c&&(c=new Bs(new ks(1,1,1),new Xs({name:"BackgroundCubeMaterial",uniforms:Vs(pa.backgroundCube.uniforms),vertexShader:pa.backgroundCube.vertexShader,fragmentShader:pa.backgroundCube.fragmentShader,side:1,depthTest:!1,depthWrite:!1,fog:!1})),c.geometry.deleteAttribute("normal"),c.geometry.deleteAttribute("uv"),c.onBeforeRender=function(t,e,n){this.matrixWorld.copyPosition(n.matrixWorld)},Object.defineProperty(c.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),r.update(c)),fa.copy(n.backgroundRotation),fa.x*=-1,fa.y*=-1,fa.z*=-1,i.isCubeTexture&&!1===i.isRenderTargetTexture&&(fa.y*=-1,fa.z*=-1),c.material.uniforms.envMap.value=i,c.material.uniforms.flipEnvMap.value=i.isCubeTexture&&!1===i.isRenderTargetTexture?-1:1,c.material.uniforms.backgroundBlurriness.value=n.backgroundBlurriness,c.material.uniforms.backgroundIntensity.value=n.backgroundIntensity,c.material.uniforms.backgroundRotation.value.setFromMatrix4(ga.makeRotationFromEuler(fa)),c.material.toneMapped=ai.getTransfer(i.colorSpace)!==Qe,u===i&&d===i.version&&p===t.toneMapping||(c.material.needsUpdate=!0,u=i,d=i.version,p=t.toneMapping),c.layers.enableAll(),e.unshift(c,c.geometry,c.material,0,0,null)):i&&i.isTexture&&(void 0===l&&(l=new Bs(new ha(2,2),new Xs({name:"BackgroundMaterial",uniforms:Vs(pa.background.uniforms),vertexShader:pa.background.vertexShader,fragmentShader:pa.background.fragmentShader,side:0,depthTest:!1,depthWrite:!1,fog:!1})),l.geometry.deleteAttribute("normal"),Object.defineProperty(l.material,"map",{get:function(){return this.uniforms.t2D.value}}),r.update(l)),l.material.uniforms.t2D.value=i,l.material.uniforms.backgroundIntensity.value=n.backgroundIntensity,l.material.toneMapped=ai.getTransfer(i.colorSpace)!==Qe,!0===i.matrixAutoUpdate&&i.updateMatrix(),l.material.uniforms.uvTransform.value.copy(i.matrix),u===i&&d===i.version&&p===t.toneMapping||(l.material.needsUpdate=!0,u=i,d=i.version,p=t.toneMapping),l.layers.enableAll(),e.unshift(l,l.geometry,l.material,0,0,null))}}}function _a(t,e){const n=t.getParameter(t.MAX_VERTEX_ATTRIBS),i={},r=c(null);let s=r,a=!1;function o(e){return t.bindVertexArray(e)}function l(e){return t.deleteVertexArray(e)}function c(t){const e=[],i=[],r=[];for(let t=0;t=0){const n=r[e];let i=a[e];if(void 0===i&&("instanceMatrix"===e&&t.instanceMatrix&&(i=t.instanceMatrix),"instanceColor"===e&&t.instanceColor&&(i=t.instanceColor)),void 0===n)return!0;if(n.attribute!==i)return!0;if(i&&n.data!==i.data)return!0;o++}}return s.attributesNum!==o||s.index!==i}(n,f,l,g),v&&function(t,e,n,i){const r={},a=e.attributes;let o=0;const l=n.getAttributes();for(const e in l){if(l[e].location>=0){let n=a[e];void 0===n&&("instanceMatrix"===e&&t.instanceMatrix&&(n=t.instanceMatrix),"instanceColor"===e&&t.instanceColor&&(n=t.instanceColor));const i={};i.attribute=n,n&&n.data&&(i.data=n.data),r[e]=i,o++}}s.attributes=r,s.attributesNum=o,s.index=i}(n,f,l,g),null!==g&&e.update(g,t.ELEMENT_ARRAY_BUFFER),(v||a)&&(a=!1,function(n,i,r,s){h();const a=s.attributes,o=r.getAttributes(),l=i.defaultAttributeValues;for(const i in o){const r=o[i];if(r.location>=0){let o=a[i];if(void 0===o&&("instanceMatrix"===i&&n.instanceMatrix&&(o=n.instanceMatrix),"instanceColor"===i&&n.instanceColor&&(o=n.instanceColor)),void 0!==o){const i=o.normalized,a=o.itemSize,l=e.get(o);if(void 0===l)continue;const c=l.buffer,h=l.type,p=l.bytesPerElement,f=h===t.INT||h===t.UNSIGNED_INT||o.gpuType===It;if(o.isInterleavedBufferAttribute){const e=o.data,l=e.stride,g=o.offset;if(e.isInstancedInterleavedBuffer){for(let t=0;t0&&t.getShaderPrecisionFormat(t.FRAGMENT_SHADER,t.HIGH_FLOAT).precision>0)return"highp";e="mediump"}return"mediump"===e&&t.getShaderPrecisionFormat(t.VERTEX_SHADER,t.MEDIUM_FLOAT).precision>0&&t.getShaderPrecisionFormat(t.FRAGMENT_SHADER,t.MEDIUM_FLOAT).precision>0?"mediump":"lowp"}let a=void 0!==n.precision?n.precision:"highp";const o=s(a);o!==a&&(console.warn("THREE.WebGLRenderer:",a,"not supported, using",o,"instead."),a=o);const l=!0===n.logarithmicDepthBuffer,c=!0===n.reverseDepthBuffer&&e.has("EXT_clip_control"),h=t.getParameter(t.MAX_TEXTURE_IMAGE_UNITS),u=t.getParameter(t.MAX_VERTEX_TEXTURE_IMAGE_UNITS);return{isWebGL2:!0,getMaxAnisotropy:function(){if(void 0!==r)return r;if(!0===e.has("EXT_texture_filter_anisotropic")){const n=e.get("EXT_texture_filter_anisotropic");r=t.getParameter(n.MAX_TEXTURE_MAX_ANISOTROPY_EXT)}else r=0;return r},getMaxPrecision:s,textureFormatReadable:function(e){return e===kt||i.convert(e)===t.getParameter(t.IMPLEMENTATION_COLOR_READ_FORMAT)},textureTypeReadable:function(n){const r=n===Ut&&(e.has("EXT_color_buffer_half_float")||e.has("EXT_color_buffer_float"));return!(n!==Et&&i.convert(n)!==t.getParameter(t.IMPLEMENTATION_COLOR_READ_TYPE)&&n!==Lt&&!r)},precision:a,logarithmicDepthBuffer:l,reverseDepthBuffer:c,maxTextures:h,maxVertexTextures:u,maxTextureSize:t.getParameter(t.MAX_TEXTURE_SIZE),maxCubemapSize:t.getParameter(t.MAX_CUBE_MAP_TEXTURE_SIZE),maxAttributes:t.getParameter(t.MAX_VERTEX_ATTRIBS),maxVertexUniforms:t.getParameter(t.MAX_VERTEX_UNIFORM_VECTORS),maxVaryings:t.getParameter(t.MAX_VARYING_VECTORS),maxFragmentUniforms:t.getParameter(t.MAX_FRAGMENT_UNIFORM_VECTORS),vertexTextures:u>0,maxSamples:t.getParameter(t.MAX_SAMPLES)}}function Ma(t){const e=this;let n=null,i=0,r=!1,s=!1;const a=new ra,o=new Kn,l={value:null,needsUpdate:!1};function c(t,n,i,r){const s=null!==t?t.length:0;let c=null;if(0!==s){if(c=l.value,!0!==r||null===c){const e=i+4*s,r=n.matrixWorldInverse;o.getNormalMatrix(r),(null===c||c.length0);e.numPlanes=i,e.numIntersection=0}();else{const t=s?0:i,e=4*t;let r=m.clippingState||null;l.value=r,r=c(u,o,e,h);for(let t=0;t!==e;++t)r[t]=n[t];m.clippingState=r,this.numIntersection=d?this.numPlanes:0,this.numPlanes+=t}}}function Sa(t){let e=new WeakMap;function n(t,e){return e===ht?t.mapping=lt:e===ut&&(t.mapping=ct),t}function i(t){const n=t.target;n.removeEventListener("dispose",i);const r=e.get(n);void 0!==r&&(e.delete(n),r.dispose())}return{get:function(r){if(r&&r.isTexture){const s=r.mapping;if(s===ht||s===ut){if(e.has(r)){return n(e.get(r).texture,r.mapping)}{const s=r.image;if(s&&s.height>0){const a=new ta(s.height);return a.fromEquirectangularTexture(t,r),e.set(r,a),r.addEventListener("dispose",i),n(a.texture,r.mapping)}return null}}}return r},dispose:function(){e=new WeakMap}}}class ba extends js{constructor(t=-1,e=1,n=1,i=-1,r=.1,s=2e3){super(),this.isOrthographicCamera=!0,this.type="OrthographicCamera",this.zoom=1,this.view=null,this.left=t,this.right=e,this.top=n,this.bottom=i,this.near=r,this.far=s,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.left=t.left,this.right=t.right,this.top=t.top,this.bottom=t.bottom,this.near=t.near,this.far=t.far,this.zoom=t.zoom,this.view=null===t.view?null:Object.assign({},t.view),this}setViewOffset(t,e,n,i,r,s){null===this.view&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=n,this.view.offsetY=i,this.view.width=r,this.view.height=s,this.updateProjectionMatrix()}clearViewOffset(){null!==this.view&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=(this.right-this.left)/(2*this.zoom),e=(this.top-this.bottom)/(2*this.zoom),n=(this.right+this.left)/2,i=(this.top+this.bottom)/2;let r=n-t,s=n+t,a=i+e,o=i-e;if(null!==this.view&&this.view.enabled){const t=(this.right-this.left)/this.view.fullWidth/this.zoom,e=(this.top-this.bottom)/this.view.fullHeight/this.zoom;r+=t*this.view.offsetX,s=r+t*this.view.width,a-=e*this.view.offsetY,o=a-e*this.view.height}this.projectionMatrix.makeOrthographic(r,s,a,o,this.near,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.zoom=this.zoom,e.object.left=this.left,e.object.right=this.right,e.object.top=this.top,e.object.bottom=this.bottom,e.object.near=this.near,e.object.far=this.far,null!==this.view&&(e.object.view=Object.assign({},this.view)),e}}const Ta=[.125,.215,.35,.446,.526,.582],wa=20,Ea=new ba,Aa=new Kr;let Ra=null,Ca=0,Ia=0,Pa=!1;const La=(1+Math.sqrt(5))/2,Ua=1/La,Da=[new Ci(-La,Ua,0),new Ci(La,Ua,0),new Ci(-Ua,0,La),new Ci(Ua,0,La),new Ci(0,La,-Ua),new Ci(0,La,Ua),new Ci(-1,1,-1),new Ci(1,1,-1),new Ci(-1,1,1),new Ci(1,1,1)];class Na{constructor(t){this._renderer=t,this._pingPongRenderTarget=null,this._lodMax=0,this._cubeSize=0,this._lodPlanes=[],this._sizeLods=[],this._sigmas=[],this._blurMaterial=null,this._cubemapMaterial=null,this._equirectMaterial=null,this._compileMaterial(this._blurMaterial)}fromScene(t,e=0,n=.1,i=100){Ra=this._renderer.getRenderTarget(),Ca=this._renderer.getActiveCubeFace(),Ia=this._renderer.getActiveMipmapLevel(),Pa=this._renderer.xr.enabled,this._renderer.xr.enabled=!1,this._setSize(256);const r=this._allocateTargets();return r.depthBuffer=!0,this._sceneToCubeUV(t,n,i,r),e>0&&this._blur(r,0,0,e),this._applyPMREM(r),this._cleanup(r),r}fromEquirectangular(t,e=null){return this._fromTexture(t,e)}fromCubemap(t,e=null){return this._fromTexture(t,e)}compileCubemapShader(){null===this._cubemapMaterial&&(this._cubemapMaterial=za(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){null===this._equirectMaterial&&(this._equirectMaterial=Ba(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),null!==this._cubemapMaterial&&this._cubemapMaterial.dispose(),null!==this._equirectMaterial&&this._equirectMaterial.dispose()}_setSize(t){this._lodMax=Math.floor(Math.log2(t)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){null!==this._blurMaterial&&this._blurMaterial.dispose(),null!==this._pingPongRenderTarget&&this._pingPongRenderTarget.dispose();for(let t=0;tt-4?o=Ta[a-t+4-1]:0===a&&(o=0),i.push(o);const l=1/(s-2),c=-l,h=1+l,u=[c,c,h,c,h,h,c,c,h,h,c,h],d=6,p=6,m=3,f=2,g=1,v=new Float32Array(m*p*d),_=new Float32Array(f*p*d),x=new Float32Array(g*p*d);for(let t=0;t2?0:-1,i=[e,n,0,e+2/3,n,0,e+2/3,n+1,0,e,n,0,e+2/3,n+1,0,e,n+1,0];v.set(i,m*p*t),_.set(u,f*p*t);const r=[t,t,t,t,t,t];x.set(r,g*p*t)}const y=new Es;y.setAttribute("position",new cs(v,m)),y.setAttribute("uv",new cs(_,f)),y.setAttribute("faceIndex",new cs(x,g)),e.push(y),r>4&&r--}return{lodPlanes:e,sizeLods:n,sigmas:i}}(i)),this._blurMaterial=function(t,e,n){const i=new Float32Array(wa),r=new Ci(0,1,0),s=new Xs({name:"SphericalGaussianBlur",defines:{n:wa,CUBEUV_TEXEL_WIDTH:1/e,CUBEUV_TEXEL_HEIGHT:1/n,CUBEUV_MAX_MIP:`${t}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:i},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:r}},vertexShader:ka(),fragmentShader:"\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\t\t\tuniform int samples;\n\t\t\tuniform float weights[ n ];\n\t\t\tuniform bool latitudinal;\n\t\t\tuniform float dTheta;\n\t\t\tuniform float mipInt;\n\t\t\tuniform vec3 poleAxis;\n\n\t\t\t#define ENVMAP_TYPE_CUBE_UV\n\t\t\t#include \n\n\t\t\tvec3 getSample( float theta, vec3 axis ) {\n\n\t\t\t\tfloat cosTheta = cos( theta );\n\t\t\t\t// Rodrigues' axis-angle rotation\n\t\t\t\tvec3 sampleDirection = vOutputDirection * cosTheta\n\t\t\t\t\t+ cross( axis, vOutputDirection ) * sin( theta )\n\t\t\t\t\t+ axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta );\n\n\t\t\t\treturn bilinearCubeUV( envMap, sampleDirection, mipInt );\n\n\t\t\t}\n\n\t\t\tvoid main() {\n\n\t\t\t\tvec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection );\n\n\t\t\t\tif ( all( equal( axis, vec3( 0.0 ) ) ) ) {\n\n\t\t\t\t\taxis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x );\n\n\t\t\t\t}\n\n\t\t\t\taxis = normalize( axis );\n\n\t\t\t\tgl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t\t\t\tgl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis );\n\n\t\t\t\tfor ( int i = 1; i < n; i++ ) {\n\n\t\t\t\t\tif ( i >= samples ) {\n\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfloat theta = dTheta * float( i );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( theta, axis );\n\n\t\t\t\t}\n\n\t\t\t}\n\t\t",blending:0,depthTest:!1,depthWrite:!1});return s}(i,t,e)}return i}_compileMaterial(t){const e=new Bs(this._lodPlanes[0],t);this._renderer.compile(e,Ea)}_sceneToCubeUV(t,e,n,i){const r=new Js(90,1,e,n),s=[1,-1,1,1,1,1],a=[1,1,1,-1,-1,-1],o=this._renderer,l=o.autoClear,c=o.toneMapping;o.getClearColor(Aa),o.toneMapping=0,o.autoClear=!1;const h=new es({name:"PMREM.Background",side:1,depthWrite:!1,depthTest:!1}),u=new Bs(new ks,h);let d=!1;const p=t.background;p?p.isColor&&(h.color.copy(p),t.background=null,d=!0):(h.color.copy(Aa),d=!0);for(let e=0;e<6;e++){const n=e%3;0===n?(r.up.set(0,s[e],0),r.lookAt(a[e],0,0)):1===n?(r.up.set(0,0,s[e]),r.lookAt(0,a[e],0)):(r.up.set(0,s[e],0),r.lookAt(0,0,a[e]));const l=this._cubeSize;Fa(i,n*l,e>2?l:0,l,l),o.setRenderTarget(i),d&&o.render(u,r),o.render(t,r)}u.geometry.dispose(),u.material.dispose(),o.toneMapping=c,o.autoClear=l,t.background=p}_textureToCubeUV(t,e){const n=this._renderer,i=t.mapping===lt||t.mapping===ct;i?(null===this._cubemapMaterial&&(this._cubemapMaterial=za()),this._cubemapMaterial.uniforms.flipEnvMap.value=!1===t.isRenderTargetTexture?-1:1):null===this._equirectMaterial&&(this._equirectMaterial=Ba());const r=i?this._cubemapMaterial:this._equirectMaterial,s=new Bs(this._lodPlanes[0],r);r.uniforms.envMap.value=t;const a=this._cubeSize;Fa(e,0,0,3*a,2*a),n.setRenderTarget(e),n.render(s,Ea)}_applyPMREM(t){const e=this._renderer,n=e.autoClear;e.autoClear=!1;const i=this._lodPlanes.length;for(let e=1;ewa&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${m} samples when the maximum is set to 20`);const f=[];let g=0;for(let t=0;tv-4?i-v+4:0),4*(this._cubeSize-_),3*_,2*_),o.setRenderTarget(e),o.render(c,Ea)}}function Oa(t,e,n){const i=new bi(t,e,n);return i.texture.mapping=dt,i.texture.name="PMREM.cubeUv",i.scissorTest=!0,i}function Fa(t,e,n,i,r){t.viewport.set(e,n,i,r),t.scissor.set(e,n,i,r)}function Ba(){return new Xs({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:ka(),fragmentShader:"\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\n\t\t\t#include \n\n\t\t\tvoid main() {\n\n\t\t\t\tvec3 outputDirection = normalize( vOutputDirection );\n\t\t\t\tvec2 uv = equirectUv( outputDirection );\n\n\t\t\t\tgl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 );\n\n\t\t\t}\n\t\t",blending:0,depthTest:!1,depthWrite:!1})}function za(){return new Xs({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:ka(),fragmentShader:"\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tuniform float flipEnvMap;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform samplerCube envMap;\n\n\t\t\tvoid main() {\n\n\t\t\t\tgl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) );\n\n\t\t\t}\n\t\t",blending:0,depthTest:!1,depthWrite:!1})}function ka(){return"\n\n\t\tprecision mediump float;\n\t\tprecision mediump int;\n\n\t\tattribute float faceIndex;\n\n\t\tvarying vec3 vOutputDirection;\n\n\t\t// RH coordinate system; PMREM face-indexing convention\n\t\tvec3 getDirection( vec2 uv, float face ) {\n\n\t\t\tuv = 2.0 * uv - 1.0;\n\n\t\t\tvec3 direction = vec3( uv, 1.0 );\n\n\t\t\tif ( face == 0.0 ) {\n\n\t\t\t\tdirection = direction.zyx; // ( 1, v, u ) pos x\n\n\t\t\t} else if ( face == 1.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xz *= -1.0; // ( -u, 1, -v ) pos y\n\n\t\t\t} else if ( face == 2.0 ) {\n\n\t\t\t\tdirection.x *= -1.0; // ( -u, v, 1 ) pos z\n\n\t\t\t} else if ( face == 3.0 ) {\n\n\t\t\t\tdirection = direction.zyx;\n\t\t\t\tdirection.xz *= -1.0; // ( -1, v, -u ) neg x\n\n\t\t\t} else if ( face == 4.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xy *= -1.0; // ( -u, -1, v ) neg y\n\n\t\t\t} else if ( face == 5.0 ) {\n\n\t\t\t\tdirection.z *= -1.0; // ( u, v, -1 ) neg z\n\n\t\t\t}\n\n\t\t\treturn direction;\n\n\t\t}\n\n\t\tvoid main() {\n\n\t\t\tvOutputDirection = getDirection( uv, faceIndex );\n\t\t\tgl_Position = vec4( position, 1.0 );\n\n\t\t}\n\t"}function Va(t){let e=new WeakMap,n=null;function i(t){const n=t.target;n.removeEventListener("dispose",i);const r=e.get(n);void 0!==r&&(e.delete(n),r.dispose())}return{get:function(r){if(r&&r.isTexture){const s=r.mapping,a=s===ht||s===ut,o=s===lt||s===ct;if(a||o){let s=e.get(r);const l=void 0!==s?s.texture.pmremVersion:0;if(r.isRenderTargetTexture&&r.pmremVersion!==l)return null===n&&(n=new Na(t)),s=a?n.fromEquirectangular(r,s):n.fromCubemap(r,s),s.texture.pmremVersion=r.pmremVersion,e.set(r,s),s.texture;if(void 0!==s)return s.texture;{const l=r.image;return a&&l&&l.height>0||o&&l&&function(t){let e=0;const n=6;for(let i=0;ie.maxTextureSize&&(y=Math.ceil(x/e.maxTextureSize),x=e.maxTextureSize);const M=new Float32Array(x*y*4*h),S=new Ti(M,x,y,h);S.type=Lt,S.needsUpdate=!0;const b=4*_;for(let w=0;w0)return t;const r=e*n;let s=to[r];if(void 0===s&&(s=new Float32Array(r),to[r]=s),0!==e){i.toArray(s,0);for(let i=1,r=0;i!==e;++i)r+=n,t[i].toArray(s,r)}return s}function ao(t,e){if(t.length!==e.length)return!1;for(let n=0,i=t.length;n":" "} ${r}: ${n[t]}`)}return i.join("\n")}(t.getShaderSource(e),i)}return r}function sl(t,e){const n=function(t){ai._getMatrix(il,ai.workingColorSpace,t);const e=`mat3( ${il.elements.map((t=>t.toFixed(4)))} )`;switch(ai.getTransfer(t)){case $e:return[e,"LinearTransferOETF"];case Qe:return[e,"sRGBTransferOETF"];default:return console.warn("THREE.WebGLProgram: Unsupported color space: ",t),[e,"LinearTransferOETF"]}}(e);return[`vec4 ${t}( vec4 value ) {`,`\treturn ${n[1]}( vec4( value.rgb * ${n[0]}, value.a ) );`,"}"].join("\n")}function al(t,e){let n;switch(e){case 1:n="Linear";break;case 2:n="Reinhard";break;case 3:n="Cineon";break;case 4:n="ACESFilmic";break;case 6:n="AgX";break;case 7:n="Neutral";break;case 5:n="Custom";break;default:console.warn("THREE.WebGLProgram: Unsupported toneMapping:",e),n="Linear"}return"vec3 "+t+"( vec3 color ) { return "+n+"ToneMapping( color ); }"}const ol=new Ci;function ll(){ai.getLuminanceCoefficients(ol);return["float luminance( const in vec3 rgb ) {",`\tconst vec3 weights = vec3( ${ol.x.toFixed(4)}, ${ol.y.toFixed(4)}, ${ol.z.toFixed(4)} );`,"\treturn dot( weights, rgb );","}"].join("\n")}function cl(t){return""!==t}function hl(t,e){const n=e.numSpotLightShadows+e.numSpotLightMaps-e.numSpotLightShadowsWithMaps;return t.replace(/NUM_DIR_LIGHTS/g,e.numDirLights).replace(/NUM_SPOT_LIGHTS/g,e.numSpotLights).replace(/NUM_SPOT_LIGHT_MAPS/g,e.numSpotLightMaps).replace(/NUM_SPOT_LIGHT_COORDS/g,n).replace(/NUM_RECT_AREA_LIGHTS/g,e.numRectAreaLights).replace(/NUM_POINT_LIGHTS/g,e.numPointLights).replace(/NUM_HEMI_LIGHTS/g,e.numHemiLights).replace(/NUM_DIR_LIGHT_SHADOWS/g,e.numDirLightShadows).replace(/NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS/g,e.numSpotLightShadowsWithMaps).replace(/NUM_SPOT_LIGHT_SHADOWS/g,e.numSpotLightShadows).replace(/NUM_POINT_LIGHT_SHADOWS/g,e.numPointLightShadows)}function ul(t,e){return t.replace(/NUM_CLIPPING_PLANES/g,e.numClippingPlanes).replace(/UNION_CLIPPING_PLANES/g,e.numClippingPlanes-e.numClipIntersection)}const dl=/^[ \t]*#include +<([\w\d./]+)>/gm;function pl(t){return t.replace(dl,fl)}const ml=new Map;function fl(t,e){let n=ua[e];if(void 0===n){const t=ml.get(e);if(void 0===t)throw new Error("Can not resolve #include <"+e+">");n=ua[t],console.warn('THREE.WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.',e,t)}return pl(n)}const gl=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function vl(t){return t.replace(gl,_l)}function _l(t,e,n,i){let r="";for(let t=parseInt(e);t0&&(g+="\n"),v=["#define SHADER_TYPE "+n.shaderType,"#define SHADER_NAME "+n.shaderName,m].filter(cl).join("\n"),v.length>0&&(v+="\n")):(g=[xl(n),"#define SHADER_TYPE "+n.shaderType,"#define SHADER_NAME "+n.shaderName,m,n.extensionClipCullDistance?"#define USE_CLIP_DISTANCE":"",n.batching?"#define USE_BATCHING":"",n.batchingColor?"#define USE_BATCHING_COLOR":"",n.instancing?"#define USE_INSTANCING":"",n.instancingColor?"#define USE_INSTANCING_COLOR":"",n.instancingMorph?"#define USE_INSTANCING_MORPH":"",n.useFog&&n.fog?"#define USE_FOG":"",n.useFog&&n.fogExp2?"#define FOG_EXP2":"",n.map?"#define USE_MAP":"",n.envMap?"#define USE_ENVMAP":"",n.envMap?"#define "+h:"",n.lightMap?"#define USE_LIGHTMAP":"",n.aoMap?"#define USE_AOMAP":"",n.bumpMap?"#define USE_BUMPMAP":"",n.normalMap?"#define USE_NORMALMAP":"",n.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",n.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",n.displacementMap?"#define USE_DISPLACEMENTMAP":"",n.emissiveMap?"#define USE_EMISSIVEMAP":"",n.anisotropy?"#define USE_ANISOTROPY":"",n.anisotropyMap?"#define USE_ANISOTROPYMAP":"",n.clearcoatMap?"#define USE_CLEARCOATMAP":"",n.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",n.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",n.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",n.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",n.specularMap?"#define USE_SPECULARMAP":"",n.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",n.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",n.roughnessMap?"#define USE_ROUGHNESSMAP":"",n.metalnessMap?"#define USE_METALNESSMAP":"",n.alphaMap?"#define USE_ALPHAMAP":"",n.alphaHash?"#define USE_ALPHAHASH":"",n.transmission?"#define USE_TRANSMISSION":"",n.transmissionMap?"#define USE_TRANSMISSIONMAP":"",n.thicknessMap?"#define USE_THICKNESSMAP":"",n.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",n.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",n.mapUv?"#define MAP_UV "+n.mapUv:"",n.alphaMapUv?"#define ALPHAMAP_UV "+n.alphaMapUv:"",n.lightMapUv?"#define LIGHTMAP_UV "+n.lightMapUv:"",n.aoMapUv?"#define AOMAP_UV "+n.aoMapUv:"",n.emissiveMapUv?"#define EMISSIVEMAP_UV "+n.emissiveMapUv:"",n.bumpMapUv?"#define BUMPMAP_UV "+n.bumpMapUv:"",n.normalMapUv?"#define NORMALMAP_UV "+n.normalMapUv:"",n.displacementMapUv?"#define DISPLACEMENTMAP_UV "+n.displacementMapUv:"",n.metalnessMapUv?"#define METALNESSMAP_UV "+n.metalnessMapUv:"",n.roughnessMapUv?"#define ROUGHNESSMAP_UV "+n.roughnessMapUv:"",n.anisotropyMapUv?"#define ANISOTROPYMAP_UV "+n.anisotropyMapUv:"",n.clearcoatMapUv?"#define CLEARCOATMAP_UV "+n.clearcoatMapUv:"",n.clearcoatNormalMapUv?"#define CLEARCOAT_NORMALMAP_UV "+n.clearcoatNormalMapUv:"",n.clearcoatRoughnessMapUv?"#define CLEARCOAT_ROUGHNESSMAP_UV "+n.clearcoatRoughnessMapUv:"",n.iridescenceMapUv?"#define IRIDESCENCEMAP_UV "+n.iridescenceMapUv:"",n.iridescenceThicknessMapUv?"#define IRIDESCENCE_THICKNESSMAP_UV "+n.iridescenceThicknessMapUv:"",n.sheenColorMapUv?"#define SHEEN_COLORMAP_UV "+n.sheenColorMapUv:"",n.sheenRoughnessMapUv?"#define SHEEN_ROUGHNESSMAP_UV "+n.sheenRoughnessMapUv:"",n.specularMapUv?"#define SPECULARMAP_UV "+n.specularMapUv:"",n.specularColorMapUv?"#define SPECULAR_COLORMAP_UV "+n.specularColorMapUv:"",n.specularIntensityMapUv?"#define SPECULAR_INTENSITYMAP_UV "+n.specularIntensityMapUv:"",n.transmissionMapUv?"#define TRANSMISSIONMAP_UV "+n.transmissionMapUv:"",n.thicknessMapUv?"#define THICKNESSMAP_UV "+n.thicknessMapUv:"",n.vertexTangents&&!1===n.flatShading?"#define USE_TANGENT":"",n.vertexColors?"#define USE_COLOR":"",n.vertexAlphas?"#define USE_COLOR_ALPHA":"",n.vertexUv1s?"#define USE_UV1":"",n.vertexUv2s?"#define USE_UV2":"",n.vertexUv3s?"#define USE_UV3":"",n.pointsUvs?"#define USE_POINTS_UV":"",n.flatShading?"#define FLAT_SHADED":"",n.skinning?"#define USE_SKINNING":"",n.morphTargets?"#define USE_MORPHTARGETS":"",n.morphNormals&&!1===n.flatShading?"#define USE_MORPHNORMALS":"",n.morphColors?"#define USE_MORPHCOLORS":"",n.morphTargetsCount>0?"#define MORPHTARGETS_TEXTURE_STRIDE "+n.morphTextureStride:"",n.morphTargetsCount>0?"#define MORPHTARGETS_COUNT "+n.morphTargetsCount:"",n.doubleSided?"#define DOUBLE_SIDED":"",n.flipSided?"#define FLIP_SIDED":"",n.shadowMapEnabled?"#define USE_SHADOWMAP":"",n.shadowMapEnabled?"#define "+l:"",n.sizeAttenuation?"#define USE_SIZEATTENUATION":"",n.numLightProbes>0?"#define USE_LIGHT_PROBES":"",n.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",n.reverseDepthBuffer?"#define USE_REVERSEDEPTHBUF":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;","#ifdef USE_INSTANCING","\tattribute mat4 instanceMatrix;","#endif","#ifdef USE_INSTANCING_COLOR","\tattribute vec3 instanceColor;","#endif","#ifdef USE_INSTANCING_MORPH","\tuniform sampler2D morphTexture;","#endif","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","#ifdef USE_UV1","\tattribute vec2 uv1;","#endif","#ifdef USE_UV2","\tattribute vec2 uv2;","#endif","#ifdef USE_UV3","\tattribute vec2 uv3;","#endif","#ifdef USE_TANGENT","\tattribute vec4 tangent;","#endif","#if defined( USE_COLOR_ALPHA )","\tattribute vec4 color;","#elif defined( USE_COLOR )","\tattribute vec3 color;","#endif","#ifdef USE_SKINNING","\tattribute vec4 skinIndex;","\tattribute vec4 skinWeight;","#endif","\n"].filter(cl).join("\n"),v=[xl(n),"#define SHADER_TYPE "+n.shaderType,"#define SHADER_NAME "+n.shaderName,m,n.useFog&&n.fog?"#define USE_FOG":"",n.useFog&&n.fogExp2?"#define FOG_EXP2":"",n.alphaToCoverage?"#define ALPHA_TO_COVERAGE":"",n.map?"#define USE_MAP":"",n.matcap?"#define USE_MATCAP":"",n.envMap?"#define USE_ENVMAP":"",n.envMap?"#define "+c:"",n.envMap?"#define "+h:"",n.envMap?"#define "+u:"",d?"#define CUBEUV_TEXEL_WIDTH "+d.texelWidth:"",d?"#define CUBEUV_TEXEL_HEIGHT "+d.texelHeight:"",d?"#define CUBEUV_MAX_MIP "+d.maxMip+".0":"",n.lightMap?"#define USE_LIGHTMAP":"",n.aoMap?"#define USE_AOMAP":"",n.bumpMap?"#define USE_BUMPMAP":"",n.normalMap?"#define USE_NORMALMAP":"",n.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",n.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",n.emissiveMap?"#define USE_EMISSIVEMAP":"",n.anisotropy?"#define USE_ANISOTROPY":"",n.anisotropyMap?"#define USE_ANISOTROPYMAP":"",n.clearcoat?"#define USE_CLEARCOAT":"",n.clearcoatMap?"#define USE_CLEARCOATMAP":"",n.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",n.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",n.dispersion?"#define USE_DISPERSION":"",n.iridescence?"#define USE_IRIDESCENCE":"",n.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",n.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",n.specularMap?"#define USE_SPECULARMAP":"",n.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",n.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",n.roughnessMap?"#define USE_ROUGHNESSMAP":"",n.metalnessMap?"#define USE_METALNESSMAP":"",n.alphaMap?"#define USE_ALPHAMAP":"",n.alphaTest?"#define USE_ALPHATEST":"",n.alphaHash?"#define USE_ALPHAHASH":"",n.sheen?"#define USE_SHEEN":"",n.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",n.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",n.transmission?"#define USE_TRANSMISSION":"",n.transmissionMap?"#define USE_TRANSMISSIONMAP":"",n.thicknessMap?"#define USE_THICKNESSMAP":"",n.vertexTangents&&!1===n.flatShading?"#define USE_TANGENT":"",n.vertexColors||n.instancingColor||n.batchingColor?"#define USE_COLOR":"",n.vertexAlphas?"#define USE_COLOR_ALPHA":"",n.vertexUv1s?"#define USE_UV1":"",n.vertexUv2s?"#define USE_UV2":"",n.vertexUv3s?"#define USE_UV3":"",n.pointsUvs?"#define USE_POINTS_UV":"",n.gradientMap?"#define USE_GRADIENTMAP":"",n.flatShading?"#define FLAT_SHADED":"",n.doubleSided?"#define DOUBLE_SIDED":"",n.flipSided?"#define FLIP_SIDED":"",n.shadowMapEnabled?"#define USE_SHADOWMAP":"",n.shadowMapEnabled?"#define "+l:"",n.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",n.numLightProbes>0?"#define USE_LIGHT_PROBES":"",n.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",n.decodeVideoTextureEmissive?"#define DECODE_VIDEO_TEXTURE_EMISSIVE":"",n.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",n.reverseDepthBuffer?"#define USE_REVERSEDEPTHBUF":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",0!==n.toneMapping?"#define TONE_MAPPING":"",0!==n.toneMapping?ua.tonemapping_pars_fragment:"",0!==n.toneMapping?al("toneMapping",n.toneMapping):"",n.dithering?"#define DITHERING":"",n.opaque?"#define OPAQUE":"",ua.colorspace_pars_fragment,sl("linearToOutputTexel",n.outputColorSpace),ll(),n.useDepthPacking?"#define DEPTH_PACKING "+n.depthPacking:"","\n"].filter(cl).join("\n")),a=pl(a),a=hl(a,n),a=ul(a,n),o=pl(o),o=hl(o,n),o=ul(o,n),a=vl(a),o=vl(o),!0!==n.isRawShaderMaterial&&(_="#version 300 es\n",g=[p,"#define attribute in","#define varying out","#define texture2D texture"].join("\n")+"\n"+g,v=["#define varying in",n.glslVersion===Nn?"":"layout(location = 0) out highp vec4 pc_fragColor;",n.glslVersion===Nn?"":"#define gl_FragColor pc_fragColor","#define gl_FragDepthEXT gl_FragDepth","#define texture2D texture","#define textureCube texture","#define texture2DProj textureProj","#define texture2DLodEXT textureLod","#define texture2DProjLodEXT textureProjLod","#define textureCubeLodEXT textureLod","#define texture2DGradEXT textureGrad","#define texture2DProjGradEXT textureProjGrad","#define textureCubeGradEXT textureGrad"].join("\n")+"\n"+v);const x=_+g+a,y=_+v+o,M=el(r,r.VERTEX_SHADER,x),S=el(r,r.FRAGMENT_SHADER,y);function b(e){if(t.debug.checkShaderErrors){const n=r.getProgramInfoLog(f).trim(),i=r.getShaderInfoLog(M).trim(),s=r.getShaderInfoLog(S).trim();let a=!0,o=!0;if(!1===r.getProgramParameter(f,r.LINK_STATUS))if(a=!1,"function"==typeof t.debug.onShaderError)t.debug.onShaderError(r,f,M,S);else{const t=rl(r,M,"vertex"),i=rl(r,S,"fragment");console.error("THREE.WebGLProgram: Shader Error "+r.getError()+" - VALIDATE_STATUS "+r.getProgramParameter(f,r.VALIDATE_STATUS)+"\n\nMaterial Name: "+e.name+"\nMaterial Type: "+e.type+"\n\nProgram Info Log: "+n+"\n"+t+"\n"+i)}else""!==n?console.warn("THREE.WebGLProgram: Program Info Log:",n):""!==i&&""!==s||(o=!1);o&&(e.diagnostics={runnable:a,programLog:n,vertexShader:{log:i,prefix:g},fragmentShader:{log:s,prefix:v}})}r.deleteShader(M),r.deleteShader(S),T=new tl(r,f),w=function(t,e){const n={},i=t.getProgramParameter(e,t.ACTIVE_ATTRIBUTES);for(let r=0;r0,q=s.clearcoat>0,Y=s.dispersion>0,Z=s.iridescence>0,J=s.sheen>0,K=s.transmission>0,$=j&&!!s.anisotropyMap,Q=q&&!!s.clearcoatMap,tt=q&&!!s.clearcoatNormalMap,et=q&&!!s.clearcoatRoughnessMap,nt=Z&&!!s.iridescenceMap,it=Z&&!!s.iridescenceThicknessMap,rt=J&&!!s.sheenColorMap,st=J&&!!s.sheenRoughnessMap,at=!!s.specularMap,ot=!!s.specularColorMap,lt=!!s.specularIntensityMap,ct=K&&!!s.transmissionMap,ht=K&&!!s.thicknessMap,ut=!!s.gradientMap,pt=!!s.alphaMap,mt=s.alphaTest>0,ft=!!s.alphaHash,gt=!!s.extensions;let vt=0;s.toneMapped&&(null!==P&&!0!==P.isXRRenderTarget||(vt=t.toneMapping));const _t={shaderID:b,shaderType:s.type,shaderName:s.name,vertexShader:E,fragmentShader:A,defines:s.defines,customVertexShaderID:R,customFragmentShaderID:C,isRawShaderMaterial:!0===s.isRawShaderMaterial,glslVersion:s.glslVersion,precision:p,batching:D,batchingColor:D&&null!==v._colorsTexture,instancing:U,instancingColor:U&&null!==v.instanceColor,instancingMorph:U&&null!==v.morphTexture,supportsVertexTextures:d,outputColorSpace:null===P?t.outputColorSpace:!0===P.isXRRenderTarget?P.texture.colorSpace:Ke,alphaToCoverage:!!s.alphaToCoverage,map:N,matcap:O,envMap:F,envMapMode:F&&M.mapping,envMapCubeUVHeight:S,aoMap:B,lightMap:z,bumpMap:k,normalMap:V,displacementMap:d&&H,emissiveMap:G,normalMapObjectSpace:V&&1===s.normalMapType,normalMapTangentSpace:V&&0===s.normalMapType,metalnessMap:W,roughnessMap:X,anisotropy:j,anisotropyMap:$,clearcoat:q,clearcoatMap:Q,clearcoatNormalMap:tt,clearcoatRoughnessMap:et,dispersion:Y,iridescence:Z,iridescenceMap:nt,iridescenceThicknessMap:it,sheen:J,sheenColorMap:rt,sheenRoughnessMap:st,specularMap:at,specularColorMap:ot,specularIntensityMap:lt,transmission:K,transmissionMap:ct,thicknessMap:ht,gradientMap:ut,opaque:!1===s.transparent&&1===s.blending&&!1===s.alphaToCoverage,alphaMap:pt,alphaTest:mt,alphaHash:ft,combine:s.combine,mapUv:N&&f(s.map.channel),aoMapUv:B&&f(s.aoMap.channel),lightMapUv:z&&f(s.lightMap.channel),bumpMapUv:k&&f(s.bumpMap.channel),normalMapUv:V&&f(s.normalMap.channel),displacementMapUv:H&&f(s.displacementMap.channel),emissiveMapUv:G&&f(s.emissiveMap.channel),metalnessMapUv:W&&f(s.metalnessMap.channel),roughnessMapUv:X&&f(s.roughnessMap.channel),anisotropyMapUv:$&&f(s.anisotropyMap.channel),clearcoatMapUv:Q&&f(s.clearcoatMap.channel),clearcoatNormalMapUv:tt&&f(s.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:et&&f(s.clearcoatRoughnessMap.channel),iridescenceMapUv:nt&&f(s.iridescenceMap.channel),iridescenceThicknessMapUv:it&&f(s.iridescenceThicknessMap.channel),sheenColorMapUv:rt&&f(s.sheenColorMap.channel),sheenRoughnessMapUv:st&&f(s.sheenRoughnessMap.channel),specularMapUv:at&&f(s.specularMap.channel),specularColorMapUv:ot&&f(s.specularColorMap.channel),specularIntensityMapUv:lt&&f(s.specularIntensityMap.channel),transmissionMapUv:ct&&f(s.transmissionMap.channel),thicknessMapUv:ht&&f(s.thicknessMap.channel),alphaMapUv:pt&&f(s.alphaMap.channel),vertexTangents:!!x.attributes.tangent&&(V||j),vertexColors:s.vertexColors,vertexAlphas:!0===s.vertexColors&&!!x.attributes.color&&4===x.attributes.color.itemSize,pointsUvs:!0===v.isPoints&&!!x.attributes.uv&&(N||pt),fog:!!_,useFog:!0===s.fog,fogExp2:!!_&&_.isFogExp2,flatShading:!0===s.flatShading,sizeAttenuation:!0===s.sizeAttenuation,logarithmicDepthBuffer:u,reverseDepthBuffer:L,skinning:!0===v.isSkinnedMesh,morphTargets:void 0!==x.morphAttributes.position,morphNormals:void 0!==x.morphAttributes.normal,morphColors:void 0!==x.morphAttributes.color,morphTargetsCount:w,morphTextureStride:I,numDirLights:o.directional.length,numPointLights:o.point.length,numSpotLights:o.spot.length,numSpotLightMaps:o.spotLightMap.length,numRectAreaLights:o.rectArea.length,numHemiLights:o.hemi.length,numDirLightShadows:o.directionalShadowMap.length,numPointLightShadows:o.pointShadowMap.length,numSpotLightShadows:o.spotShadowMap.length,numSpotLightShadowsWithMaps:o.numSpotLightShadowsWithMaps,numLightProbes:o.numLightProbes,numClippingPlanes:a.numPlanes,numClipIntersection:a.numIntersection,dithering:s.dithering,shadowMapEnabled:t.shadowMap.enabled&&h.length>0,shadowMapType:t.shadowMap.type,toneMapping:vt,decodeVideoTexture:N&&!0===s.map.isVideoTexture&&ai.getTransfer(s.map.colorSpace)===Qe,decodeVideoTextureEmissive:G&&!0===s.emissiveMap.isVideoTexture&&ai.getTransfer(s.emissiveMap.colorSpace)===Qe,premultipliedAlpha:s.premultipliedAlpha,doubleSided:2===s.side,flipSided:1===s.side,useDepthPacking:s.depthPacking>=0,depthPacking:s.depthPacking||0,index0AttributeName:s.index0AttributeName,extensionClipCullDistance:gt&&!0===s.extensions.clipCullDistance&&i.has("WEBGL_clip_cull_distance"),extensionMultiDraw:(gt&&!0===s.extensions.multiDraw||D)&&i.has("WEBGL_multi_draw"),rendererExtensionParallelShaderCompile:i.has("KHR_parallel_shader_compile"),customProgramCacheKey:s.customProgramCacheKey()};return _t.vertexUv1s=c.has(1),_t.vertexUv2s=c.has(2),_t.vertexUv3s=c.has(3),c.clear(),_t},getProgramCacheKey:function(e){const n=[];if(e.shaderID?n.push(e.shaderID):(n.push(e.customVertexShaderID),n.push(e.customFragmentShaderID)),void 0!==e.defines)for(const t in e.defines)n.push(t),n.push(e.defines[t]);return!1===e.isRawShaderMaterial&&(!function(t,e){t.push(e.precision),t.push(e.outputColorSpace),t.push(e.envMapMode),t.push(e.envMapCubeUVHeight),t.push(e.mapUv),t.push(e.alphaMapUv),t.push(e.lightMapUv),t.push(e.aoMapUv),t.push(e.bumpMapUv),t.push(e.normalMapUv),t.push(e.displacementMapUv),t.push(e.emissiveMapUv),t.push(e.metalnessMapUv),t.push(e.roughnessMapUv),t.push(e.anisotropyMapUv),t.push(e.clearcoatMapUv),t.push(e.clearcoatNormalMapUv),t.push(e.clearcoatRoughnessMapUv),t.push(e.iridescenceMapUv),t.push(e.iridescenceThicknessMapUv),t.push(e.sheenColorMapUv),t.push(e.sheenRoughnessMapUv),t.push(e.specularMapUv),t.push(e.specularColorMapUv),t.push(e.specularIntensityMapUv),t.push(e.transmissionMapUv),t.push(e.thicknessMapUv),t.push(e.combine),t.push(e.fogExp2),t.push(e.sizeAttenuation),t.push(e.morphTargetsCount),t.push(e.morphAttributeCount),t.push(e.numDirLights),t.push(e.numPointLights),t.push(e.numSpotLights),t.push(e.numSpotLightMaps),t.push(e.numHemiLights),t.push(e.numRectAreaLights),t.push(e.numDirLightShadows),t.push(e.numPointLightShadows),t.push(e.numSpotLightShadows),t.push(e.numSpotLightShadowsWithMaps),t.push(e.numLightProbes),t.push(e.shadowMapType),t.push(e.toneMapping),t.push(e.numClippingPlanes),t.push(e.numClipIntersection),t.push(e.depthPacking)}(n,e),function(t,e){o.disableAll(),e.supportsVertexTextures&&o.enable(0);e.instancing&&o.enable(1);e.instancingColor&&o.enable(2);e.instancingMorph&&o.enable(3);e.matcap&&o.enable(4);e.envMap&&o.enable(5);e.normalMapObjectSpace&&o.enable(6);e.normalMapTangentSpace&&o.enable(7);e.clearcoat&&o.enable(8);e.iridescence&&o.enable(9);e.alphaTest&&o.enable(10);e.vertexColors&&o.enable(11);e.vertexAlphas&&o.enable(12);e.vertexUv1s&&o.enable(13);e.vertexUv2s&&o.enable(14);e.vertexUv3s&&o.enable(15);e.vertexTangents&&o.enable(16);e.anisotropy&&o.enable(17);e.alphaHash&&o.enable(18);e.batching&&o.enable(19);e.dispersion&&o.enable(20);e.batchingColor&&o.enable(21);t.push(o.mask),o.disableAll(),e.fog&&o.enable(0);e.useFog&&o.enable(1);e.flatShading&&o.enable(2);e.logarithmicDepthBuffer&&o.enable(3);e.reverseDepthBuffer&&o.enable(4);e.skinning&&o.enable(5);e.morphTargets&&o.enable(6);e.morphNormals&&o.enable(7);e.morphColors&&o.enable(8);e.premultipliedAlpha&&o.enable(9);e.shadowMapEnabled&&o.enable(10);e.doubleSided&&o.enable(11);e.flipSided&&o.enable(12);e.useDepthPacking&&o.enable(13);e.dithering&&o.enable(14);e.transmission&&o.enable(15);e.sheen&&o.enable(16);e.opaque&&o.enable(17);e.pointsUvs&&o.enable(18);e.decodeVideoTexture&&o.enable(19);e.decodeVideoTextureEmissive&&o.enable(20);e.alphaToCoverage&&o.enable(21);t.push(o.mask)}(n,e),n.push(t.outputColorSpace)),n.push(e.customProgramCacheKey),n.join()},getUniforms:function(t){const e=m[t.type];let n;if(e){const t=pa[e];n=Ws.clone(t.uniforms)}else n=t.uniforms;return n},acquireProgram:function(e,n){let i;for(let t=0,e=h.length;t0?i.push(h):!0===a.transparent?r.push(h):n.push(h)},unshift:function(t,e,a,o,l,c){const h=s(t,e,a,o,l,c);a.transmission>0?i.unshift(h):!0===a.transparent?r.unshift(h):n.unshift(h)},finish:function(){for(let n=e,i=t.length;n1&&n.sort(t||El),i.length>1&&i.sort(e||Al),r.length>1&&r.sort(e||Al)}}}function Cl(){let t=new WeakMap;return{get:function(e,n){const i=t.get(e);let r;return void 0===i?(r=new Rl,t.set(e,[r])):n>=i.length?(r=new Rl,i.push(r)):r=i[n],r},dispose:function(){t=new WeakMap}}}function Il(){const t={};return{get:function(e){if(void 0!==t[e.id])return t[e.id];let n;switch(e.type){case"DirectionalLight":n={direction:new Ci,color:new Kr};break;case"SpotLight":n={position:new Ci,direction:new Ci,color:new Kr,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":n={position:new Ci,color:new Kr,distance:0,decay:0};break;case"HemisphereLight":n={direction:new Ci,skyColor:new Kr,groundColor:new Kr};break;case"RectAreaLight":n={color:new Kr,position:new Ci,halfWidth:new Ci,halfHeight:new Ci}}return t[e.id]=n,n}}}let Pl=0;function Ll(t,e){return(e.castShadow?2:0)-(t.castShadow?2:0)+(e.map?1:0)-(t.map?1:0)}function Ul(t){const e=new Il,n=function(){const t={};return{get:function(e){if(void 0!==t[e.id])return t[e.id];let n;switch(e.type){case"DirectionalLight":case"SpotLight":n={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Jn};break;case"PointLight":n={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Jn,shadowCameraNear:1,shadowCameraFar:1e3}}return t[e.id]=n,n}}}(),i={version:0,hash:{directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1,numLightProbes:-1},ambient:[0,0,0],probe:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0,numLightProbes:0};for(let t=0;t<9;t++)i.probe.push(new Ci);const r=new Ci,s=new sr,a=new sr;return{setup:function(r){let s=0,a=0,o=0;for(let t=0;t<9;t++)i.probe[t].set(0,0,0);let l=0,c=0,h=0,u=0,d=0,p=0,m=0,f=0,g=0,v=0,_=0;r.sort(Ll);for(let t=0,x=r.length;t0&&(!0===t.has("OES_texture_float_linear")?(i.rectAreaLTC1=da.LTC_FLOAT_1,i.rectAreaLTC2=da.LTC_FLOAT_2):(i.rectAreaLTC1=da.LTC_HALF_1,i.rectAreaLTC2=da.LTC_HALF_2)),i.ambient[0]=s,i.ambient[1]=a,i.ambient[2]=o;const x=i.hash;x.directionalLength===l&&x.pointLength===c&&x.spotLength===h&&x.rectAreaLength===u&&x.hemiLength===d&&x.numDirectionalShadows===p&&x.numPointShadows===m&&x.numSpotShadows===f&&x.numSpotMaps===g&&x.numLightProbes===_||(i.directional.length=l,i.spot.length=h,i.rectArea.length=u,i.point.length=c,i.hemi.length=d,i.directionalShadow.length=p,i.directionalShadowMap.length=p,i.pointShadow.length=m,i.pointShadowMap.length=m,i.spotShadow.length=f,i.spotShadowMap.length=f,i.directionalShadowMatrix.length=p,i.pointShadowMatrix.length=m,i.spotLightMatrix.length=f+g-v,i.spotLightMap.length=g,i.numSpotLightShadowsWithMaps=v,i.numLightProbes=_,x.directionalLength=l,x.pointLength=c,x.spotLength=h,x.rectAreaLength=u,x.hemiLength=d,x.numDirectionalShadows=p,x.numPointShadows=m,x.numSpotShadows=f,x.numSpotMaps=g,x.numLightProbes=_,i.version=Pl++)},setupView:function(t,e){let n=0,o=0,l=0,c=0,h=0;const u=e.matrixWorldInverse;for(let e=0,d=t.length;e=r.length?(s=new Dl(t),r.push(s)):s=r[i],s},dispose:function(){e=new WeakMap}}}class Ol extends ts{static get type(){return"MeshDepthMaterial"}constructor(t){super(),this.isMeshDepthMaterial=!0,this.depthPacking=3200,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.setValues(t)}copy(t){return super.copy(t),this.depthPacking=t.depthPacking,this.map=t.map,this.alphaMap=t.alphaMap,this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this}}class Fl extends ts{static get type(){return"MeshDistanceMaterial"}constructor(t){super(),this.isMeshDistanceMaterial=!0,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.setValues(t)}copy(t){return super.copy(t),this.map=t.map,this.alphaMap=t.alphaMap,this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this}}function Bl(t,e,n){let i=new oa;const r=new Jn,s=new Jn,a=new Mi,o=new Ol({depthPacking:3201}),l=new Fl,c={},h=n.maxTextureSize,m={[u]:1,[d]:0,[p]:2},f=new Xs({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new Jn},radius:{value:4}},vertexShader:"void main() {\n\tgl_Position = vec4( position, 1.0 );\n}",fragmentShader:"uniform sampler2D shadow_pass;\nuniform vec2 resolution;\nuniform float radius;\n#include \nvoid main() {\n\tconst float samples = float( VSM_SAMPLES );\n\tfloat mean = 0.0;\n\tfloat squared_mean = 0.0;\n\tfloat uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 );\n\tfloat uvStart = samples <= 1.0 ? 0.0 : - 1.0;\n\tfor ( float i = 0.0; i < samples; i ++ ) {\n\t\tfloat uvOffset = uvStart + i * uvStride;\n\t\t#ifdef HORIZONTAL_PASS\n\t\t\tvec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ) );\n\t\t\tmean += distribution.x;\n\t\t\tsquared_mean += distribution.y * distribution.y + distribution.x * distribution.x;\n\t\t#else\n\t\t\tfloat depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ) );\n\t\t\tmean += depth;\n\t\t\tsquared_mean += depth * depth;\n\t\t#endif\n\t}\n\tmean = mean / samples;\n\tsquared_mean = squared_mean / samples;\n\tfloat std_dev = sqrt( squared_mean - mean * mean );\n\tgl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) );\n}"}),g=f.clone();g.defines.HORIZONTAL_PASS=1;const v=new Es;v.setAttribute("position",new cs(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const _=new Bs(v,f),x=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=1;let y=this.type;function M(n,i){const s=e.update(_);f.defines.VSM_SAMPLES!==n.blurSamples&&(f.defines.VSM_SAMPLES=n.blurSamples,g.defines.VSM_SAMPLES=n.blurSamples,f.needsUpdate=!0,g.needsUpdate=!0),null===n.mapPass&&(n.mapPass=new bi(r.x,r.y)),f.uniforms.shadow_pass.value=n.map.texture,f.uniforms.resolution.value=n.mapSize,f.uniforms.radius.value=n.radius,t.setRenderTarget(n.mapPass),t.clear(),t.renderBufferDirect(i,null,s,f,_,null),g.uniforms.shadow_pass.value=n.mapPass.texture,g.uniforms.resolution.value=n.mapSize,g.uniforms.radius.value=n.radius,t.setRenderTarget(n.map),t.clear(),t.renderBufferDirect(i,null,s,g,_,null)}function S(e,n,i,r){let s=null;const a=!0===i.isPointLight?e.customDistanceMaterial:e.customDepthMaterial;if(void 0!==a)s=a;else if(s=!0===i.isPointLight?l:o,t.localClippingEnabled&&!0===n.clipShadows&&Array.isArray(n.clippingPlanes)&&0!==n.clippingPlanes.length||n.displacementMap&&0!==n.displacementScale||n.alphaMap&&n.alphaTest>0||n.map&&n.alphaTest>0){const t=s.uuid,e=n.uuid;let i=c[t];void 0===i&&(i={},c[t]=i);let r=i[e];void 0===r&&(r=s.clone(),i[e]=r,n.addEventListener("dispose",T)),s=r}if(s.visible=n.visible,s.wireframe=n.wireframe,s.side=3===r?null!==n.shadowSide?n.shadowSide:n.side:null!==n.shadowSide?n.shadowSide:m[n.side],s.alphaMap=n.alphaMap,s.alphaTest=n.alphaTest,s.map=n.map,s.clipShadows=n.clipShadows,s.clippingPlanes=n.clippingPlanes,s.clipIntersection=n.clipIntersection,s.displacementMap=n.displacementMap,s.displacementScale=n.displacementScale,s.displacementBias=n.displacementBias,s.wireframeLinewidth=n.wireframeLinewidth,s.linewidth=n.linewidth,!0===i.isPointLight&&!0===s.isMeshDistanceMaterial){t.properties.get(s).light=i}return s}function b(n,r,s,a,o){if(!1===n.visible)return;if(n.layers.test(r.layers)&&(n.isMesh||n.isLine||n.isPoints)&&(n.castShadow||n.receiveShadow&&3===o)&&(!n.frustumCulled||i.intersectsObject(n))){n.modelViewMatrix.multiplyMatrices(s.matrixWorldInverse,n.matrixWorld);const i=e.update(n),l=n.material;if(Array.isArray(l)){const e=i.groups;for(let c=0,h=e.length;ch||r.y>h)&&(r.x>h&&(s.x=Math.floor(h/f.x),r.x=s.x*f.x,u.mapSize.x=s.x),r.y>h&&(s.y=Math.floor(h/f.y),r.y=s.y*f.y,u.mapSize.y=s.y)),null===u.map||!0===p||!0===m){const t=3!==this.type?{minFilter:gt,magFilter:gt}:{};null!==u.map&&u.map.dispose(),u.map=new bi(r.x,r.y,t),u.map.texture.name=c.name+".shadowMap",u.camera.updateProjectionMatrix()}t.setRenderTarget(u.map),t.clear();const g=u.getViewportCount();for(let t=0;t=1):-1!==Z.indexOf("OpenGL ES")&&(Y=parseFloat(/^OpenGL ES (\d)/.exec(Z)[1]),q=Y>=2);let J=null,K={};const $=t.getParameter(t.SCISSOR_BOX),Q=t.getParameter(t.VIEWPORT),tt=(new Mi).fromArray($),et=(new Mi).fromArray(Q);function nt(e,n,i,r){const s=new Uint8Array(4),a=t.createTexture();t.bindTexture(e,a),t.texParameteri(e,t.TEXTURE_MIN_FILTER,t.NEAREST),t.texParameteri(e,t.TEXTURE_MAG_FILTER,t.NEAREST);for(let a=0;ae?(t.repeat.x=1,t.repeat.y=n/e,t.offset.x=0,t.offset.y=(1-t.repeat.y)/2):(t.repeat.x=e/n,t.repeat.y=1,t.offset.x=(1-t.repeat.x)/2,t.offset.y=0),t},cover:function(t,e){const n=t.image&&t.image.width?t.image.width/t.image.height:1;return n>e?(t.repeat.x=e/n,t.repeat.y=1,t.offset.x=(1-t.repeat.x)/2,t.offset.y=0):(t.repeat.x=1,t.repeat.y=n/e,t.offset.x=0,t.offset.y=(1-t.repeat.y)/2),t},fill:function(t){return t.repeat.x=1,t.repeat.y=1,t.offset.x=0,t.offset.y=0,t},getByteLength:Vl};function Gl(t,e,n,i,r,s,a){const o=e.has("WEBGL_multisampled_render_to_texture")?e.get("WEBGL_multisampled_render_to_texture"):null,l="undefined"!=typeof navigator&&/OculusBrowser/g.test(navigator.userAgent),c=new Jn,h=new WeakMap;let u;const d=new WeakMap;let p=!1;try{p="undefined"!=typeof OffscreenCanvas&&null!==new OffscreenCanvas(1,1).getContext("2d")}catch(t){}function m(t,e){return p?new OffscreenCanvas(t,e):ni("canvas")}function f(t,e,n){let i=1;const r=V(t);if((r.width>n||r.height>n)&&(i=n/Math.max(r.width,r.height)),i<1){if("undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&t instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap||"undefined"!=typeof VideoFrame&&t instanceof VideoFrame){const n=Math.floor(i*r.width),s=Math.floor(i*r.height);void 0===u&&(u=m(n,s));const a=e?m(n,s):u;a.width=n,a.height=s;return a.getContext("2d").drawImage(t,0,0,n,s),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+r.width+"x"+r.height+") to ("+n+"x"+s+")."),a}return"data"in t&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+r.width+"x"+r.height+")."),t}return t}function g(t){return t.generateMipmaps}function v(e){t.generateMipmap(e)}function _(e){return e.isWebGLCubeRenderTarget?t.TEXTURE_CUBE_MAP:e.isWebGL3DRenderTarget?t.TEXTURE_3D:e.isWebGLArrayRenderTarget||e.isCompressedArrayTexture?t.TEXTURE_2D_ARRAY:t.TEXTURE_2D}function x(n,i,r,s,a=!1){if(null!==n){if(void 0!==t[n])return t[n];console.warn("THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format '"+n+"'")}let o=i;if(i===t.RED&&(r===t.FLOAT&&(o=t.R32F),r===t.HALF_FLOAT&&(o=t.R16F),r===t.UNSIGNED_BYTE&&(o=t.R8)),i===t.RED_INTEGER&&(r===t.UNSIGNED_BYTE&&(o=t.R8UI),r===t.UNSIGNED_SHORT&&(o=t.R16UI),r===t.UNSIGNED_INT&&(o=t.R32UI),r===t.BYTE&&(o=t.R8I),r===t.SHORT&&(o=t.R16I),r===t.INT&&(o=t.R32I)),i===t.RG&&(r===t.FLOAT&&(o=t.RG32F),r===t.HALF_FLOAT&&(o=t.RG16F),r===t.UNSIGNED_BYTE&&(o=t.RG8)),i===t.RG_INTEGER&&(r===t.UNSIGNED_BYTE&&(o=t.RG8UI),r===t.UNSIGNED_SHORT&&(o=t.RG16UI),r===t.UNSIGNED_INT&&(o=t.RG32UI),r===t.BYTE&&(o=t.RG8I),r===t.SHORT&&(o=t.RG16I),r===t.INT&&(o=t.RG32I)),i===t.RGB_INTEGER&&(r===t.UNSIGNED_BYTE&&(o=t.RGB8UI),r===t.UNSIGNED_SHORT&&(o=t.RGB16UI),r===t.UNSIGNED_INT&&(o=t.RGB32UI),r===t.BYTE&&(o=t.RGB8I),r===t.SHORT&&(o=t.RGB16I),r===t.INT&&(o=t.RGB32I)),i===t.RGBA_INTEGER&&(r===t.UNSIGNED_BYTE&&(o=t.RGBA8UI),r===t.UNSIGNED_SHORT&&(o=t.RGBA16UI),r===t.UNSIGNED_INT&&(o=t.RGBA32UI),r===t.BYTE&&(o=t.RGBA8I),r===t.SHORT&&(o=t.RGBA16I),r===t.INT&&(o=t.RGBA32I)),i===t.RGB&&r===t.UNSIGNED_INT_5_9_9_9_REV&&(o=t.RGB9_E5),i===t.RGBA){const e=a?$e:ai.getTransfer(s);r===t.FLOAT&&(o=t.RGBA32F),r===t.HALF_FLOAT&&(o=t.RGBA16F),r===t.UNSIGNED_BYTE&&(o=e===Qe?t.SRGB8_ALPHA8:t.RGBA8),r===t.UNSIGNED_SHORT_4_4_4_4&&(o=t.RGBA4),r===t.UNSIGNED_SHORT_5_5_5_1&&(o=t.RGB5_A1)}return o!==t.R16F&&o!==t.R32F&&o!==t.RG16F&&o!==t.RG32F&&o!==t.RGBA16F&&o!==t.RGBA32F||e.get("EXT_color_buffer_float"),o}function y(e,n){let i;return e?null===n||n===Pt||n===Ot?i=t.DEPTH24_STENCIL8:n===Lt?i=t.DEPTH32F_STENCIL8:n===Ct&&(i=t.DEPTH24_STENCIL8,console.warn("DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.")):null===n||n===Pt||n===Ot?i=t.DEPTH_COMPONENT24:n===Lt?i=t.DEPTH_COMPONENT32F:n===Ct&&(i=t.DEPTH_COMPONENT16),i}function M(t,e){return!0===g(t)||t.isFramebufferTexture&&t.minFilter!==gt&&t.minFilter!==Mt?Math.log2(Math.max(e.width,e.height))+1:void 0!==t.mipmaps&&t.mipmaps.length>0?t.mipmaps.length:t.isCompressedTexture&&Array.isArray(t.image)?e.mipmaps.length:1}function S(t){const e=t.target;e.removeEventListener("dispose",S),function(t){const e=i.get(t);if(void 0===e.__webglInit)return;const n=t.source,r=d.get(n);if(r){const i=r[e.__cacheKey];i.usedTimes--,0===i.usedTimes&&T(t),0===Object.keys(r).length&&d.delete(n)}i.remove(t)}(e),e.isVideoTexture&&h.delete(e)}function b(e){const n=e.target;n.removeEventListener("dispose",b),function(e){const n=i.get(e);e.depthTexture&&(e.depthTexture.dispose(),i.remove(e.depthTexture));if(e.isWebGLCubeRenderTarget)for(let e=0;e<6;e++){if(Array.isArray(n.__webglFramebuffer[e]))for(let i=0;i0&&s.__version!==e.version){const t=e.image;if(null===t)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else{if(!1!==t.complete)return void L(s,e,r);console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete")}}n.bindTexture(t.TEXTURE_2D,s.__webglTexture,t.TEXTURE0+r)}const A={[pt]:t.REPEAT,[mt]:t.CLAMP_TO_EDGE,[ft]:t.MIRRORED_REPEAT},R={[gt]:t.NEAREST,[vt]:t.NEAREST_MIPMAP_NEAREST,[xt]:t.NEAREST_MIPMAP_LINEAR,[Mt]:t.LINEAR,[St]:t.LINEAR_MIPMAP_NEAREST,[Tt]:t.LINEAR_MIPMAP_LINEAR},C={[vn]:t.NEVER,[Tn]:t.ALWAYS,[_n]:t.LESS,[yn]:t.LEQUAL,[xn]:t.EQUAL,[bn]:t.GEQUAL,[Mn]:t.GREATER,[Sn]:t.NOTEQUAL};function I(n,s){if(s.type!==Lt||!1!==e.has("OES_texture_float_linear")||s.magFilter!==Mt&&s.magFilter!==St&&s.magFilter!==xt&&s.magFilter!==Tt&&s.minFilter!==Mt&&s.minFilter!==St&&s.minFilter!==xt&&s.minFilter!==Tt||console.warn("THREE.WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device."),t.texParameteri(n,t.TEXTURE_WRAP_S,A[s.wrapS]),t.texParameteri(n,t.TEXTURE_WRAP_T,A[s.wrapT]),n!==t.TEXTURE_3D&&n!==t.TEXTURE_2D_ARRAY||t.texParameteri(n,t.TEXTURE_WRAP_R,A[s.wrapR]),t.texParameteri(n,t.TEXTURE_MAG_FILTER,R[s.magFilter]),t.texParameteri(n,t.TEXTURE_MIN_FILTER,R[s.minFilter]),s.compareFunction&&(t.texParameteri(n,t.TEXTURE_COMPARE_MODE,t.COMPARE_REF_TO_TEXTURE),t.texParameteri(n,t.TEXTURE_COMPARE_FUNC,C[s.compareFunction])),!0===e.has("EXT_texture_filter_anisotropic")){if(s.magFilter===gt)return;if(s.minFilter!==xt&&s.minFilter!==Tt)return;if(s.type===Lt&&!1===e.has("OES_texture_float_linear"))return;if(s.anisotropy>1||i.get(s).__currentAnisotropy){const a=e.get("EXT_texture_filter_anisotropic");t.texParameterf(n,a.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(s.anisotropy,r.getMaxAnisotropy())),i.get(s).__currentAnisotropy=s.anisotropy}}}function P(e,n){let i=!1;void 0===e.__webglInit&&(e.__webglInit=!0,n.addEventListener("dispose",S));const r=n.source;let s=d.get(r);void 0===s&&(s={},d.set(r,s));const o=function(t){const e=[];return e.push(t.wrapS),e.push(t.wrapT),e.push(t.wrapR||0),e.push(t.magFilter),e.push(t.minFilter),e.push(t.anisotropy),e.push(t.internalFormat),e.push(t.format),e.push(t.type),e.push(t.generateMipmaps),e.push(t.premultiplyAlpha),e.push(t.flipY),e.push(t.unpackAlignment),e.push(t.colorSpace),e.join()}(n);if(o!==e.__cacheKey){void 0===s[o]&&(s[o]={texture:t.createTexture(),usedTimes:0},a.memory.textures++,i=!0),s[o].usedTimes++;const r=s[e.__cacheKey];void 0!==r&&(s[e.__cacheKey].usedTimes--,0===r.usedTimes&&T(n)),e.__cacheKey=o,e.__webglTexture=s[o].texture}return i}function L(e,a,o){let l=t.TEXTURE_2D;(a.isDataArrayTexture||a.isCompressedArrayTexture)&&(l=t.TEXTURE_2D_ARRAY),a.isData3DTexture&&(l=t.TEXTURE_3D);const c=P(e,a),h=a.source;n.bindTexture(l,e.__webglTexture,t.TEXTURE0+o);const u=i.get(h);if(h.version!==u.__version||!0===c){n.activeTexture(t.TEXTURE0+o);const e=ai.getPrimaries(ai.workingColorSpace),i=a.colorSpace===Ze?null:ai.getPrimaries(a.colorSpace),d=a.colorSpace===Ze||e===i?t.NONE:t.BROWSER_DEFAULT_WEBGL;t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,a.flipY),t.pixelStorei(t.UNPACK_PREMULTIPLY_ALPHA_WEBGL,a.premultiplyAlpha),t.pixelStorei(t.UNPACK_ALIGNMENT,a.unpackAlignment),t.pixelStorei(t.UNPACK_COLORSPACE_CONVERSION_WEBGL,d);let p=f(a.image,!1,r.maxTextureSize);p=k(a,p);const m=s.convert(a.format,a.colorSpace),_=s.convert(a.type);let S,b=x(a.internalFormat,m,_,a.colorSpace,a.isVideoTexture);I(l,a);const T=a.mipmaps,w=!0!==a.isVideoTexture,E=void 0===u.__version||!0===c,A=h.dataReady,R=M(a,p);if(a.isDepthTexture)b=y(a.format===Wt,a.type),E&&(w?n.texStorage2D(t.TEXTURE_2D,1,b,p.width,p.height):n.texImage2D(t.TEXTURE_2D,0,b,p.width,p.height,0,m,_,null));else if(a.isDataTexture)if(T.length>0){w&&E&&n.texStorage2D(t.TEXTURE_2D,R,b,T[0].width,T[0].height);for(let e=0,i=T.length;e0){const i=Vl(S.width,S.height,a.format,a.type);for(const r of a.layerUpdates){const s=S.data.subarray(r*i/S.data.BYTES_PER_ELEMENT,(r+1)*i/S.data.BYTES_PER_ELEMENT);n.compressedTexSubImage3D(t.TEXTURE_2D_ARRAY,e,0,0,r,S.width,S.height,1,m,s)}a.clearLayerUpdates()}else n.compressedTexSubImage3D(t.TEXTURE_2D_ARRAY,e,0,0,0,S.width,S.height,p.depth,m,S.data)}else n.compressedTexImage3D(t.TEXTURE_2D_ARRAY,e,b,S.width,S.height,p.depth,0,S.data,0,0);else console.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()");else w?A&&n.texSubImage3D(t.TEXTURE_2D_ARRAY,e,0,0,0,S.width,S.height,p.depth,m,_,S.data):n.texImage3D(t.TEXTURE_2D_ARRAY,e,b,S.width,S.height,p.depth,0,m,_,S.data)}else{w&&E&&n.texStorage2D(t.TEXTURE_2D,R,b,T[0].width,T[0].height);for(let e=0,i=T.length;e0){const e=Vl(p.width,p.height,a.format,a.type);for(const i of a.layerUpdates){const r=p.data.subarray(i*e/p.data.BYTES_PER_ELEMENT,(i+1)*e/p.data.BYTES_PER_ELEMENT);n.texSubImage3D(t.TEXTURE_2D_ARRAY,0,0,0,i,p.width,p.height,1,m,_,r)}a.clearLayerUpdates()}else n.texSubImage3D(t.TEXTURE_2D_ARRAY,0,0,0,0,p.width,p.height,p.depth,m,_,p.data)}else n.texImage3D(t.TEXTURE_2D_ARRAY,0,b,p.width,p.height,p.depth,0,m,_,p.data);else if(a.isData3DTexture)w?(E&&n.texStorage3D(t.TEXTURE_3D,R,b,p.width,p.height,p.depth),A&&n.texSubImage3D(t.TEXTURE_3D,0,0,0,0,p.width,p.height,p.depth,m,_,p.data)):n.texImage3D(t.TEXTURE_3D,0,b,p.width,p.height,p.depth,0,m,_,p.data);else if(a.isFramebufferTexture){if(E)if(w)n.texStorage2D(t.TEXTURE_2D,R,b,p.width,p.height);else{let e=p.width,i=p.height;for(let r=0;r>=1,i>>=1}}else if(T.length>0){if(w&&E){const e=V(T[0]);n.texStorage2D(t.TEXTURE_2D,R,b,e.width,e.height)}for(let e=0,i=T.length;e>h),i=Math.max(1,r.height>>h);c===t.TEXTURE_3D||c===t.TEXTURE_2D_ARRAY?n.texImage3D(c,h,p,e,i,r.depth,0,u,d,null):n.texImage2D(c,h,p,e,i,0,u,d,null)}n.bindFramebuffer(t.FRAMEBUFFER,e),z(r)?o.framebufferTexture2DMultisampleEXT(t.FRAMEBUFFER,l,c,f.__webglTexture,0,B(r)):(c===t.TEXTURE_2D||c>=t.TEXTURE_CUBE_MAP_POSITIVE_X&&c<=t.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&t.framebufferTexture2D(t.FRAMEBUFFER,l,c,f.__webglTexture,h),n.bindFramebuffer(t.FRAMEBUFFER,null)}function D(e,n,i){if(t.bindRenderbuffer(t.RENDERBUFFER,e),n.depthBuffer){const r=n.depthTexture,s=r&&r.isDepthTexture?r.type:null,a=y(n.stencilBuffer,s),l=n.stencilBuffer?t.DEPTH_STENCIL_ATTACHMENT:t.DEPTH_ATTACHMENT,c=B(n);z(n)?o.renderbufferStorageMultisampleEXT(t.RENDERBUFFER,c,a,n.width,n.height):i?t.renderbufferStorageMultisample(t.RENDERBUFFER,c,a,n.width,n.height):t.renderbufferStorage(t.RENDERBUFFER,a,n.width,n.height),t.framebufferRenderbuffer(t.FRAMEBUFFER,l,t.RENDERBUFFER,e)}else{const e=n.textures;for(let r=0;r{delete r.__boundDepthTexture,delete r.__depthDisposeCallback,t.removeEventListener("dispose",e)};t.addEventListener("dispose",e),r.__depthDisposeCallback=e}r.__boundDepthTexture=t}if(e.depthTexture&&!r.__autoAllocateDepthBuffer){if(s)throw new Error("target.depthTexture not supported in Cube render targets");!function(e,r){if(r&&r.isWebGLCubeRenderTarget)throw new Error("Depth Texture with cube render targets is not supported");if(n.bindFramebuffer(t.FRAMEBUFFER,e),!r.depthTexture||!r.depthTexture.isDepthTexture)throw new Error("renderTarget.depthTexture must be an instance of THREE.DepthTexture");const s=i.get(r.depthTexture);s.__renderTarget=r,s.__webglTexture&&r.depthTexture.image.width===r.width&&r.depthTexture.image.height===r.height||(r.depthTexture.image.width=r.width,r.depthTexture.image.height=r.height,r.depthTexture.needsUpdate=!0),E(r.depthTexture,0);const a=s.__webglTexture,l=B(r);if(r.depthTexture.format===Gt)z(r)?o.framebufferTexture2DMultisampleEXT(t.FRAMEBUFFER,t.DEPTH_ATTACHMENT,t.TEXTURE_2D,a,0,l):t.framebufferTexture2D(t.FRAMEBUFFER,t.DEPTH_ATTACHMENT,t.TEXTURE_2D,a,0);else{if(r.depthTexture.format!==Wt)throw new Error("Unknown depthTexture format");z(r)?o.framebufferTexture2DMultisampleEXT(t.FRAMEBUFFER,t.DEPTH_STENCIL_ATTACHMENT,t.TEXTURE_2D,a,0,l):t.framebufferTexture2D(t.FRAMEBUFFER,t.DEPTH_STENCIL_ATTACHMENT,t.TEXTURE_2D,a,0)}}(r.__webglFramebuffer,e)}else if(s){r.__webglDepthbuffer=[];for(let i=0;i<6;i++)if(n.bindFramebuffer(t.FRAMEBUFFER,r.__webglFramebuffer[i]),void 0===r.__webglDepthbuffer[i])r.__webglDepthbuffer[i]=t.createRenderbuffer(),D(r.__webglDepthbuffer[i],e,!1);else{const n=e.stencilBuffer?t.DEPTH_STENCIL_ATTACHMENT:t.DEPTH_ATTACHMENT,s=r.__webglDepthbuffer[i];t.bindRenderbuffer(t.RENDERBUFFER,s),t.framebufferRenderbuffer(t.FRAMEBUFFER,n,t.RENDERBUFFER,s)}}else if(n.bindFramebuffer(t.FRAMEBUFFER,r.__webglFramebuffer),void 0===r.__webglDepthbuffer)r.__webglDepthbuffer=t.createRenderbuffer(),D(r.__webglDepthbuffer,e,!1);else{const n=e.stencilBuffer?t.DEPTH_STENCIL_ATTACHMENT:t.DEPTH_ATTACHMENT,i=r.__webglDepthbuffer;t.bindRenderbuffer(t.RENDERBUFFER,i),t.framebufferRenderbuffer(t.FRAMEBUFFER,n,t.RENDERBUFFER,i)}n.bindFramebuffer(t.FRAMEBUFFER,null)}const O=[],F=[];function B(t){return Math.min(r.maxSamples,t.samples)}function z(t){const n=i.get(t);return t.samples>0&&!0===e.has("WEBGL_multisampled_render_to_texture")&&!1!==n.__useRenderToTexture}function k(t,e){const n=t.colorSpace,i=t.format,r=t.type;return!0===t.isCompressedTexture||!0===t.isVideoTexture||n!==Ke&&n!==Ze&&(ai.getTransfer(n)===Qe?i===kt&&r===Et||console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture color space:",n)),e}function V(t){return"undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement?(c.width=t.naturalWidth||t.width,c.height=t.naturalHeight||t.height):"undefined"!=typeof VideoFrame&&t instanceof VideoFrame?(c.width=t.displayWidth,c.height=t.displayHeight):(c.width=t.width,c.height=t.height),c}this.allocateTextureUnit=function(){const t=w;return t>=r.maxTextures&&console.warn("THREE.WebGLTextures: Trying to use "+t+" texture units while this GPU supports only "+r.maxTextures),w+=1,t},this.resetTextureUnits=function(){w=0},this.setTexture2D=E,this.setTexture2DArray=function(e,r){const s=i.get(e);e.version>0&&s.__version!==e.version?L(s,e,r):n.bindTexture(t.TEXTURE_2D_ARRAY,s.__webglTexture,t.TEXTURE0+r)},this.setTexture3D=function(e,r){const s=i.get(e);e.version>0&&s.__version!==e.version?L(s,e,r):n.bindTexture(t.TEXTURE_3D,s.__webglTexture,t.TEXTURE0+r)},this.setTextureCube=function(e,a){const o=i.get(e);e.version>0&&o.__version!==e.version?function(e,a,o){if(6!==a.image.length)return;const l=P(e,a),c=a.source;n.bindTexture(t.TEXTURE_CUBE_MAP,e.__webglTexture,t.TEXTURE0+o);const h=i.get(c);if(c.version!==h.__version||!0===l){n.activeTexture(t.TEXTURE0+o);const e=ai.getPrimaries(ai.workingColorSpace),i=a.colorSpace===Ze?null:ai.getPrimaries(a.colorSpace),u=a.colorSpace===Ze||e===i?t.NONE:t.BROWSER_DEFAULT_WEBGL;t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,a.flipY),t.pixelStorei(t.UNPACK_PREMULTIPLY_ALPHA_WEBGL,a.premultiplyAlpha),t.pixelStorei(t.UNPACK_ALIGNMENT,a.unpackAlignment),t.pixelStorei(t.UNPACK_COLORSPACE_CONVERSION_WEBGL,u);const d=a.isCompressedTexture||a.image[0].isCompressedTexture,p=a.image[0]&&a.image[0].isDataTexture,m=[];for(let t=0;t<6;t++)m[t]=d||p?p?a.image[t].image:a.image[t]:f(a.image[t],!0,r.maxCubemapSize),m[t]=k(a,m[t]);const _=m[0],y=s.convert(a.format,a.colorSpace),S=s.convert(a.type),b=x(a.internalFormat,y,S,a.colorSpace),T=!0!==a.isVideoTexture,w=void 0===h.__version||!0===l,E=c.dataReady;let A,R=M(a,_);if(I(t.TEXTURE_CUBE_MAP,a),d){T&&w&&n.texStorage2D(t.TEXTURE_CUBE_MAP,R,b,_.width,_.height);for(let e=0;e<6;e++){A=m[e].mipmaps;for(let i=0;i0&&R++;const e=V(m[0]);n.texStorage2D(t.TEXTURE_CUBE_MAP,R,b,e.width,e.height)}for(let e=0;e<6;e++)if(p){T?E&&n.texSubImage2D(t.TEXTURE_CUBE_MAP_POSITIVE_X+e,0,0,0,m[e].width,m[e].height,y,S,m[e].data):n.texImage2D(t.TEXTURE_CUBE_MAP_POSITIVE_X+e,0,b,m[e].width,m[e].height,0,y,S,m[e].data);for(let i=0;i1;if(u||(void 0===l.__webglTexture&&(l.__webglTexture=t.createTexture()),l.__version=r.version,a.memory.textures++),h){o.__webglFramebuffer=[];for(let e=0;e<6;e++)if(r.mipmaps&&r.mipmaps.length>0){o.__webglFramebuffer[e]=[];for(let n=0;n0){o.__webglFramebuffer=[];for(let e=0;e0&&!1===z(e)){o.__webglMultisampledFramebuffer=t.createFramebuffer(),o.__webglColorRenderbuffer=[],n.bindFramebuffer(t.FRAMEBUFFER,o.__webglMultisampledFramebuffer);for(let n=0;n0)for(let i=0;i0)for(let n=0;n0)if(!1===z(e)){const r=e.textures,s=e.width,a=e.height;let o=t.COLOR_BUFFER_BIT;const c=e.stencilBuffer?t.DEPTH_STENCIL_ATTACHMENT:t.DEPTH_ATTACHMENT,h=i.get(e),u=r.length>1;if(u)for(let e=0;eo+c?(l.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!l.inputState.pinching&&a<=o-c&&(l.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:t.handedness,target:this}))}else null!==o&&t.gripSpace&&(r=e.getPose(t.gripSpace,n),null!==r&&(o.matrix.fromArray(r.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,r.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(r.linearVelocity)):o.hasLinearVelocity=!1,r.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(r.angularVelocity)):o.hasAngularVelocity=!1));null!==a&&(i=e.getPose(t.targetRaySpace,n),null===i&&null!==r&&(i=r),null!==i&&(a.matrix.fromArray(i.transform.matrix),a.matrix.decompose(a.position,a.rotation,a.scale),a.matrixWorldNeedsUpdate=!0,i.linearVelocity?(a.hasLinearVelocity=!0,a.linearVelocity.copy(i.linearVelocity)):a.hasLinearVelocity=!1,i.angularVelocity?(a.hasAngularVelocity=!0,a.angularVelocity.copy(i.angularVelocity)):a.hasAngularVelocity=!1,this.dispatchEvent(ql)))}return null!==a&&(a.visible=null!==i),null!==o&&(o.visible=null!==r),null!==l&&(l.visible=null!==s),this}_getHandJoint(t,e){if(void 0===t.joints[e.jointName]){const n=new jl;n.matrixAutoUpdate=!1,n.visible=!1,t.joints[e.jointName]=n,t.add(n)}return t.joints[e.jointName]}}class Zl{constructor(){this.texture=null,this.mesh=null,this.depthNear=0,this.depthFar=0}init(t,e,n){if(null===this.texture){const i=new yi;t.properties.get(i).__webglTexture=e.texture,e.depthNear==n.depthNear&&e.depthFar==n.depthFar||(this.depthNear=e.depthNear,this.depthFar=e.depthFar),this.texture=i}}getMesh(t){if(null!==this.texture&&null===this.mesh){const e=t.cameras[0].viewport,n=new Xs({vertexShader:"\nvoid main() {\n\n\tgl_Position = vec4( position, 1.0 );\n\n}",fragmentShader:"\nuniform sampler2DArray depthColor;\nuniform float depthWidth;\nuniform float depthHeight;\n\nvoid main() {\n\n\tvec2 coord = vec2( gl_FragCoord.x / depthWidth, gl_FragCoord.y / depthHeight );\n\n\tif ( coord.x >= 1.0 ) {\n\n\t\tgl_FragDepth = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r;\n\n\t} else {\n\n\t\tgl_FragDepth = texture( depthColor, vec3( coord.x, coord.y, 0 ) ).r;\n\n\t}\n\n}",uniforms:{depthColor:{value:this.texture},depthWidth:{value:e.z},depthHeight:{value:e.w}}});this.mesh=new Bs(new ha(20,20),n)}return this.mesh}reset(){this.texture=null,this.mesh=null}getDepthTexture(){return this.texture}}class Jl extends Bn{constructor(t,e){super();const n=this;let i=null,r=1,s=null,a="local-floor",o=1,l=null,c=null,h=null,u=null,d=null,p=null;const m=new Zl,f=e.getContextAttributes();let g=null,v=null;const _=[],x=[],y=new Jn;let M=null;const S=new Js;S.viewport=new Mi;const b=new Js;b.viewport=new Mi;const T=[S,b],w=new Xl;let E=null,A=null;function R(t){const e=x.indexOf(t.inputSource);if(-1===e)return;const n=_[e];void 0!==n&&(n.update(t.inputSource,t.frame,l||s),n.dispatchEvent({type:t.type,data:t.inputSource}))}function C(){i.removeEventListener("select",R),i.removeEventListener("selectstart",R),i.removeEventListener("selectend",R),i.removeEventListener("squeeze",R),i.removeEventListener("squeezestart",R),i.removeEventListener("squeezeend",R),i.removeEventListener("end",C),i.removeEventListener("inputsourceschange",I);for(let t=0;t<_.length;t++){const e=x[t];null!==e&&(x[t]=null,_[t].disconnect(e))}E=null,A=null,m.reset(),t.setRenderTarget(g),d=null,u=null,h=null,i=null,v=null,N.stop(),n.isPresenting=!1,t.setPixelRatio(M),t.setSize(y.width,y.height,!1),n.dispatchEvent({type:"sessionend"})}function I(t){for(let e=0;e=0&&(x[i]=null,_[i].disconnect(n))}for(let e=0;e=x.length){x.push(n),i=t;break}if(null===x[t]){x[t]=n,i=t;break}}if(-1===i)break}const r=_[i];r&&r.connect(n)}}this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(t){let e=_[t];return void 0===e&&(e=new Yl,_[t]=e),e.getTargetRaySpace()},this.getControllerGrip=function(t){let e=_[t];return void 0===e&&(e=new Yl,_[t]=e),e.getGripSpace()},this.getHand=function(t){let e=_[t];return void 0===e&&(e=new Yl,_[t]=e),e.getHandSpace()},this.setFramebufferScaleFactor=function(t){r=t,!0===n.isPresenting&&console.warn("THREE.WebXRManager: Cannot change framebuffer scale while presenting.")},this.setReferenceSpaceType=function(t){a=t,!0===n.isPresenting&&console.warn("THREE.WebXRManager: Cannot change reference space type while presenting.")},this.getReferenceSpace=function(){return l||s},this.setReferenceSpace=function(t){l=t},this.getBaseLayer=function(){return null!==u?u:d},this.getBinding=function(){return h},this.getFrame=function(){return p},this.getSession=function(){return i},this.setSession=async function(c){if(i=c,null!==i){if(g=t.getRenderTarget(),i.addEventListener("select",R),i.addEventListener("selectstart",R),i.addEventListener("selectend",R),i.addEventListener("squeeze",R),i.addEventListener("squeezestart",R),i.addEventListener("squeezeend",R),i.addEventListener("end",C),i.addEventListener("inputsourceschange",I),!0!==f.xrCompatible&&await e.makeXRCompatible(),M=t.getPixelRatio(),t.getSize(y),void 0===i.renderState.layers){const n={antialias:f.antialias,alpha:!0,depth:f.depth,stencil:f.stencil,framebufferScaleFactor:r};d=new XRWebGLLayer(i,e,n),i.updateRenderState({baseLayer:d}),t.setPixelRatio(1),t.setSize(d.framebufferWidth,d.framebufferHeight,!1),v=new bi(d.framebufferWidth,d.framebufferHeight,{format:kt,type:Et,colorSpace:t.outputColorSpace,stencilBuffer:f.stencil})}else{let n=null,s=null,a=null;f.depth&&(a=f.stencil?e.DEPTH24_STENCIL8:e.DEPTH_COMPONENT24,n=f.stencil?Wt:Gt,s=f.stencil?Ot:Pt);const o={colorFormat:e.RGBA8,depthFormat:a,scaleFactor:r};h=new XRWebGLBinding(i,e),u=h.createProjectionLayer(o),i.updateRenderState({layers:[u]}),t.setPixelRatio(1),t.setSize(u.textureWidth,u.textureHeight,!1),v=new bi(u.textureWidth,u.textureHeight,{format:kt,type:Et,depthTexture:new Ya(u.textureWidth,u.textureHeight,s,void 0,void 0,void 0,void 0,void 0,void 0,n),stencilBuffer:f.stencil,colorSpace:t.outputColorSpace,samples:f.antialias?4:0,resolveDepthBuffer:!1===u.ignoreDepthValues})}v.isXRRenderTarget=!0,this.setFoveation(o),l=null,s=await i.requestReferenceSpace(a),N.setContext(i),N.start(),n.isPresenting=!0,n.dispatchEvent({type:"sessionstart"})}},this.getEnvironmentBlendMode=function(){if(null!==i)return i.environmentBlendMode},this.getDepthTexture=function(){return m.getDepthTexture()};const P=new Ci,L=new Ci;function U(t,e){null===e?t.matrixWorld.copy(t.matrix):t.matrixWorld.multiplyMatrices(e.matrixWorld,t.matrix),t.matrixWorldInverse.copy(t.matrixWorld).invert()}this.updateCamera=function(t){if(null===i)return;let e=t.near,n=t.far;null!==m.texture&&(m.depthNear>0&&(e=m.depthNear),m.depthFar>0&&(n=m.depthFar)),w.near=b.near=S.near=e,w.far=b.far=S.far=n,E===w.near&&A===w.far||(i.updateRenderState({depthNear:w.near,depthFar:w.far}),E=w.near,A=w.far),S.layers.mask=2|t.layers.mask,b.layers.mask=4|t.layers.mask,w.layers.mask=S.layers.mask|b.layers.mask;const r=t.parent,s=w.cameras;U(w,r);for(let t=0;t0&&(t.alphaTest.value=i.alphaTest);const r=e.get(i),s=r.envMap,a=r.envMapRotation;s&&(t.envMap.value=s,Kl.copy(a),Kl.x*=-1,Kl.y*=-1,Kl.z*=-1,s.isCubeTexture&&!1===s.isRenderTargetTexture&&(Kl.y*=-1,Kl.z*=-1),t.envMapRotation.value.setFromMatrix4($l.makeRotationFromEuler(Kl)),t.flipEnvMap.value=s.isCubeTexture&&!1===s.isRenderTargetTexture?-1:1,t.reflectivity.value=i.reflectivity,t.ior.value=i.ior,t.refractionRatio.value=i.refractionRatio),i.lightMap&&(t.lightMap.value=i.lightMap,t.lightMapIntensity.value=i.lightMapIntensity,n(i.lightMap,t.lightMapTransform)),i.aoMap&&(t.aoMap.value=i.aoMap,t.aoMapIntensity.value=i.aoMapIntensity,n(i.aoMap,t.aoMapTransform))}return{refreshFogUniforms:function(e,n){n.color.getRGB(e.fogColor.value,Gs(t)),n.isFog?(e.fogNear.value=n.near,e.fogFar.value=n.far):n.isFogExp2&&(e.fogDensity.value=n.density)},refreshMaterialUniforms:function(t,r,s,a,o){r.isMeshBasicMaterial||r.isMeshLambertMaterial?i(t,r):r.isMeshToonMaterial?(i(t,r),function(t,e){e.gradientMap&&(t.gradientMap.value=e.gradientMap)}(t,r)):r.isMeshPhongMaterial?(i(t,r),function(t,e){t.specular.value.copy(e.specular),t.shininess.value=Math.max(e.shininess,1e-4)}(t,r)):r.isMeshStandardMaterial?(i(t,r),function(t,e){t.metalness.value=e.metalness,e.metalnessMap&&(t.metalnessMap.value=e.metalnessMap,n(e.metalnessMap,t.metalnessMapTransform));t.roughness.value=e.roughness,e.roughnessMap&&(t.roughnessMap.value=e.roughnessMap,n(e.roughnessMap,t.roughnessMapTransform));e.envMap&&(t.envMapIntensity.value=e.envMapIntensity)}(t,r),r.isMeshPhysicalMaterial&&function(t,e,i){t.ior.value=e.ior,e.sheen>0&&(t.sheenColor.value.copy(e.sheenColor).multiplyScalar(e.sheen),t.sheenRoughness.value=e.sheenRoughness,e.sheenColorMap&&(t.sheenColorMap.value=e.sheenColorMap,n(e.sheenColorMap,t.sheenColorMapTransform)),e.sheenRoughnessMap&&(t.sheenRoughnessMap.value=e.sheenRoughnessMap,n(e.sheenRoughnessMap,t.sheenRoughnessMapTransform)));e.clearcoat>0&&(t.clearcoat.value=e.clearcoat,t.clearcoatRoughness.value=e.clearcoatRoughness,e.clearcoatMap&&(t.clearcoatMap.value=e.clearcoatMap,n(e.clearcoatMap,t.clearcoatMapTransform)),e.clearcoatRoughnessMap&&(t.clearcoatRoughnessMap.value=e.clearcoatRoughnessMap,n(e.clearcoatRoughnessMap,t.clearcoatRoughnessMapTransform)),e.clearcoatNormalMap&&(t.clearcoatNormalMap.value=e.clearcoatNormalMap,n(e.clearcoatNormalMap,t.clearcoatNormalMapTransform),t.clearcoatNormalScale.value.copy(e.clearcoatNormalScale),1===e.side&&t.clearcoatNormalScale.value.negate()));e.dispersion>0&&(t.dispersion.value=e.dispersion);e.iridescence>0&&(t.iridescence.value=e.iridescence,t.iridescenceIOR.value=e.iridescenceIOR,t.iridescenceThicknessMinimum.value=e.iridescenceThicknessRange[0],t.iridescenceThicknessMaximum.value=e.iridescenceThicknessRange[1],e.iridescenceMap&&(t.iridescenceMap.value=e.iridescenceMap,n(e.iridescenceMap,t.iridescenceMapTransform)),e.iridescenceThicknessMap&&(t.iridescenceThicknessMap.value=e.iridescenceThicknessMap,n(e.iridescenceThicknessMap,t.iridescenceThicknessMapTransform)));e.transmission>0&&(t.transmission.value=e.transmission,t.transmissionSamplerMap.value=i.texture,t.transmissionSamplerSize.value.set(i.width,i.height),e.transmissionMap&&(t.transmissionMap.value=e.transmissionMap,n(e.transmissionMap,t.transmissionMapTransform)),t.thickness.value=e.thickness,e.thicknessMap&&(t.thicknessMap.value=e.thicknessMap,n(e.thicknessMap,t.thicknessMapTransform)),t.attenuationDistance.value=e.attenuationDistance,t.attenuationColor.value.copy(e.attenuationColor));e.anisotropy>0&&(t.anisotropyVector.value.set(e.anisotropy*Math.cos(e.anisotropyRotation),e.anisotropy*Math.sin(e.anisotropyRotation)),e.anisotropyMap&&(t.anisotropyMap.value=e.anisotropyMap,n(e.anisotropyMap,t.anisotropyMapTransform)));t.specularIntensity.value=e.specularIntensity,t.specularColor.value.copy(e.specularColor),e.specularColorMap&&(t.specularColorMap.value=e.specularColorMap,n(e.specularColorMap,t.specularColorMapTransform));e.specularIntensityMap&&(t.specularIntensityMap.value=e.specularIntensityMap,n(e.specularIntensityMap,t.specularIntensityMapTransform))}(t,r,o)):r.isMeshMatcapMaterial?(i(t,r),function(t,e){e.matcap&&(t.matcap.value=e.matcap)}(t,r)):r.isMeshDepthMaterial?i(t,r):r.isMeshDistanceMaterial?(i(t,r),function(t,n){const i=e.get(n).light;t.referencePosition.value.setFromMatrixPosition(i.matrixWorld),t.nearDistance.value=i.shadow.camera.near,t.farDistance.value=i.shadow.camera.far}(t,r)):r.isMeshNormalMaterial?i(t,r):r.isLineBasicMaterial?(function(t,e){t.diffuse.value.copy(e.color),t.opacity.value=e.opacity,e.map&&(t.map.value=e.map,n(e.map,t.mapTransform))}(t,r),r.isLineDashedMaterial&&function(t,e){t.dashSize.value=e.dashSize,t.totalSize.value=e.dashSize+e.gapSize,t.scale.value=e.scale}(t,r)):r.isPointsMaterial?function(t,e,i,r){t.diffuse.value.copy(e.color),t.opacity.value=e.opacity,t.size.value=e.size*i,t.scale.value=.5*r,e.map&&(t.map.value=e.map,n(e.map,t.uvTransform));e.alphaMap&&(t.alphaMap.value=e.alphaMap,n(e.alphaMap,t.alphaMapTransform));e.alphaTest>0&&(t.alphaTest.value=e.alphaTest)}(t,r,s,a):r.isSpriteMaterial?function(t,e){t.diffuse.value.copy(e.color),t.opacity.value=e.opacity,t.rotation.value=e.rotation,e.map&&(t.map.value=e.map,n(e.map,t.mapTransform));e.alphaMap&&(t.alphaMap.value=e.alphaMap,n(e.alphaMap,t.alphaMapTransform));e.alphaTest>0&&(t.alphaTest.value=e.alphaTest)}(t,r):r.isShadowMaterial?(t.color.value.copy(r.color),t.opacity.value=r.opacity):r.isShaderMaterial&&(r.uniformsNeedUpdate=!1)}}}function tc(t,e,n,i){let r={},s={},a=[];const o=t.getParameter(t.MAX_UNIFORM_BUFFER_BINDINGS);function l(t,e,n,i){const r=t.value,s=e+"_"+n;if(void 0===i[s])return i[s]="number"==typeof r||"boolean"==typeof r?r:r.clone(),!0;{const t=i[s];if("number"==typeof r||"boolean"==typeof r){if(t!==r)return i[s]=r,!0}else if(!1===t.equals(r))return t.copy(r),!0}return!1}function c(t){const e={boundary:0,storage:0};return"number"==typeof t||"boolean"==typeof t?(e.boundary=4,e.storage=4):t.isVector2?(e.boundary=8,e.storage=8):t.isVector3||t.isColor?(e.boundary=16,e.storage=12):t.isVector4?(e.boundary=16,e.storage=16):t.isMatrix3?(e.boundary=48,e.storage=48):t.isMatrix4?(e.boundary=64,e.storage=64):t.isTexture?console.warn("THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group."):console.warn("THREE.WebGLRenderer: Unsupported uniform value type.",t),e}function h(e){const n=e.target;n.removeEventListener("dispose",h);const i=a.indexOf(n.__bindingPointIndex);a.splice(i,1),t.deleteBuffer(r[n.id]),delete r[n.id],delete s[n.id]}return{bind:function(t,e){const n=e.program;i.uniformBlockBinding(t,n)},update:function(n,u){let d=r[n.id];void 0===d&&(!function(t){const e=t.uniforms;let n=0;const i=16;for(let t=0,r=e.length;t0&&(n+=i-r);t.__size=n,t.__cache={}}(n),d=function(e){const n=function(){for(let t=0;t0),u=!!n.morphAttributes.position,d=!!n.morphAttributes.normal,p=!!n.morphAttributes.color;let m=0;i.toneMapped&&(null!==T&&!0!==T.isXRRenderTarget||(m=y.toneMapping));const f=n.morphAttributes.position||n.morphAttributes.normal||n.morphAttributes.color,g=void 0!==f?f.length:0,_=tt.get(i),x=v.state.lights;if(!0===V&&(!0===H||t!==E)){const e=t===E&&i.id===w;ut.setState(i,t,e)}let M=!1;i.version===_.__version?_.needsLights&&_.lightsStateVersion!==x.state.version||_.outputColorSpace!==o||r.isBatchedMesh&&!1===_.batching?M=!0:r.isBatchedMesh||!0!==_.batching?r.isBatchedMesh&&!0===_.batchingColor&&null===r.colorTexture||r.isBatchedMesh&&!1===_.batchingColor&&null!==r.colorTexture||r.isInstancedMesh&&!1===_.instancing?M=!0:r.isInstancedMesh||!0!==_.instancing?r.isSkinnedMesh&&!1===_.skinning?M=!0:r.isSkinnedMesh||!0!==_.skinning?r.isInstancedMesh&&!0===_.instancingColor&&null===r.instanceColor||r.isInstancedMesh&&!1===_.instancingColor&&null!==r.instanceColor||r.isInstancedMesh&&!0===_.instancingMorph&&null===r.morphTexture||r.isInstancedMesh&&!1===_.instancingMorph&&null!==r.morphTexture||_.envMap!==l||!0===i.fog&&_.fog!==s?M=!0:void 0===_.numClippingPlanes||_.numClippingPlanes===ut.numPlanes&&_.numIntersection===ut.numIntersection?(_.vertexAlphas!==c||_.vertexTangents!==h||_.morphTargets!==u||_.morphNormals!==d||_.morphColors!==p||_.toneMapping!==m||_.morphTargetsCount!==g)&&(M=!0):M=!0:M=!0:M=!0:M=!0:(M=!0,_.__version=i.version);let S=_.currentProgram;!0===M&&(S=qt(i,e,r));let b=!1,A=!1,R=!1;const C=S.getUniforms(),I=_.uniforms;$.useProgram(S.program)&&(b=!0,A=!0,R=!0);i.id!==w&&(w=i.id,A=!0);if(b||E!==t){$.buffers.depth.getReversed()?(G.copy(t.projectionMatrix),function(t){const e=t.elements;e[2]=.5*e[2]+.5*e[3],e[6]=.5*e[6]+.5*e[7],e[10]=.5*e[10]+.5*e[11],e[14]=.5*e[14]+.5*e[15]}(G),function(t){const e=t.elements;-1===e[11]?(e[10]=-e[10]-1,e[14]=-e[14]):(e[10]=-e[10],e[14]=1-e[14])}(G),C.setValue(yt,"projectionMatrix",G)):C.setValue(yt,"projectionMatrix",t.projectionMatrix),C.setValue(yt,"viewMatrix",t.matrixWorldInverse);const e=C.map.cameraPosition;void 0!==e&&e.setValue(yt,X.setFromMatrixPosition(t.matrixWorld)),K.logarithmicDepthBuffer&&C.setValue(yt,"logDepthBufFC",2/(Math.log(t.far+1)/Math.LN2)),(i.isMeshPhongMaterial||i.isMeshToonMaterial||i.isMeshLambertMaterial||i.isMeshBasicMaterial||i.isMeshStandardMaterial||i.isShaderMaterial)&&C.setValue(yt,"isOrthographic",!0===t.isOrthographicCamera),E!==t&&(E=t,A=!0,R=!0)}if(r.isSkinnedMesh){C.setOptional(yt,r,"bindMatrix"),C.setOptional(yt,r,"bindMatrixInverse");const t=r.skeleton;t&&(null===t.boneTexture&&t.computeBoneTexture(),C.setValue(yt,"boneTexture",t.boneTexture,et))}r.isBatchedMesh&&(C.setOptional(yt,r,"batchingTexture"),C.setValue(yt,"batchingTexture",r._matricesTexture,et),C.setOptional(yt,r,"batchingIdTexture"),C.setValue(yt,"batchingIdTexture",r._indirectTexture,et),C.setOptional(yt,r,"batchingColorTexture"),null!==r._colorsTexture&&C.setValue(yt,"batchingColorTexture",r._colorsTexture,et));const P=n.morphAttributes;void 0===P.position&&void 0===P.normal&&void 0===P.color||mt.update(r,n,S);(A||_.receiveShadow!==r.receiveShadow)&&(_.receiveShadow=r.receiveShadow,C.setValue(yt,"receiveShadow",r.receiveShadow));i.isMeshGouraudMaterial&&null!==i.envMap&&(I.envMap.value=l,I.flipEnvMap.value=l.isCubeTexture&&!1===l.isRenderTargetTexture?-1:1);i.isMeshStandardMaterial&&null===i.envMap&&null!==e.environment&&(I.envMapIntensity.value=e.environmentIntensity);A&&(C.setValue(yt,"toneMappingExposure",y.toneMappingExposure),_.needsLights&&(N=R,(L=I).ambientLightColor.needsUpdate=N,L.lightProbe.needsUpdate=N,L.directionalLights.needsUpdate=N,L.directionalLightShadows.needsUpdate=N,L.pointLights.needsUpdate=N,L.pointLightShadows.needsUpdate=N,L.spotLights.needsUpdate=N,L.spotLightShadows.needsUpdate=N,L.rectAreaLights.needsUpdate=N,L.hemisphereLights.needsUpdate=N),s&&!0===i.fog&<.refreshFogUniforms(I,s),lt.refreshMaterialUniforms(I,i,D,U,v.state.transmissionRenderTarget[t.id]),tl.upload(yt,Zt(_),I,et));var L,N;i.isShaderMaterial&&!0===i.uniformsNeedUpdate&&(tl.upload(yt,Zt(_),I,et),i.uniformsNeedUpdate=!1);i.isSpriteMaterial&&C.setValue(yt,"center",r.center);if(C.setValue(yt,"modelViewMatrix",r.modelViewMatrix),C.setValue(yt,"normalMatrix",r.normalMatrix),C.setValue(yt,"modelMatrix",r.matrixWorld),i.isShaderMaterial||i.isRawShaderMaterial){const t=i.uniformsGroups;for(let e=0,n=t.length;e{function n(){i.forEach((function(t){tt.get(t).currentProgram.isReady()&&i.delete(t)})),0!==i.size?setTimeout(n,10):e(t)}null!==J.get("KHR_parallel_shader_compile")?n():setTimeout(n,10)}))};let Ft=null;function Bt(){kt.stop()}function zt(){kt.start()}const kt=new la;function Vt(t,e,n,i){if(!1===t.visible)return;if(t.layers.test(e.layers))if(t.isGroup)n=t.renderOrder;else if(t.isLOD)!0===t.autoUpdate&&t.update(e);else if(t.isLight)v.pushLight(t),t.castShadow&&v.pushShadow(t);else if(t.isSprite){if(!t.frustumCulled||k.intersectsSprite(t)){i&&j.setFromMatrixPosition(t.matrixWorld).applyMatrix4(W);const e=at.update(t),r=t.material;r.visible&&g.push(t,e,r,n,j.z,null)}}else if((t.isMesh||t.isLine||t.isPoints)&&(!t.frustumCulled||k.intersectsObject(t))){const e=at.update(t),r=t.material;if(i&&(void 0!==t.boundingSphere?(null===t.boundingSphere&&t.computeBoundingSphere(),j.copy(t.boundingSphere.center)):(null===e.boundingSphere&&e.computeBoundingSphere(),j.copy(e.boundingSphere.center)),j.applyMatrix4(t.matrixWorld).applyMatrix4(W)),Array.isArray(r)){const i=e.groups;for(let s=0,a=i.length;s0&&Wt(r,e,n),s.length>0&&Wt(s,e,n),a.length>0&&Wt(a,e,n),$.buffers.depth.setTest(!0),$.buffers.depth.setMask(!0),$.buffers.color.setMask(!0),$.setPolygonOffset(!1)}function Gt(t,e,n,i){if(null!==(!0===n.isScene?n.overrideMaterial:null))return;void 0===v.state.transmissionRenderTarget[i.id]&&(v.state.transmissionRenderTarget[i.id]=new bi(1,1,{generateMipmaps:!0,type:J.has("EXT_color_buffer_half_float")||J.has("EXT_color_buffer_float")?Ut:Et,minFilter:Tt,samples:4,stencilBuffer:s,resolveDepthBuffer:!1,resolveStencilBuffer:!1,colorSpace:ai.workingColorSpace}));const r=v.state.transmissionRenderTarget[i.id],a=i.viewport||A;r.setSize(a.z,a.w);const o=y.getRenderTarget();y.setRenderTarget(r),y.getClearColor(I),P=y.getClearAlpha(),P<1&&y.setClearColor(16777215,.5),y.clear(),Y&&pt.render(n);const l=y.toneMapping;y.toneMapping=0;const c=i.viewport;if(void 0!==i.viewport&&(i.viewport=void 0),v.setupLightsView(i),!0===V&&ut.setGlobalState(y.clippingPlanes,i),Wt(t,n,i),et.updateMultisampleRenderTarget(r),et.updateRenderTargetMipmap(r),!1===J.has("WEBGL_multisampled_render_to_texture")){let t=!1;for(let r=0,s=e.length;r0)for(let e=0,s=n.length;e0&&Gt(i,r,t,e),Y&&pt.render(t),Ht(g,t,e);null!==T&&(et.updateMultisampleRenderTarget(T),et.updateRenderTargetMipmap(T)),!0===t.isScene&&t.onAfterRender(y,t,e),_t.resetDefaultState(),w=-1,E=null,x.pop(),x.length>0?(v=x[x.length-1],!0===V&&ut.setGlobalState(y.clippingPlanes,v.state.camera)):v=null,_.pop(),g=_.length>0?_[_.length-1]:null},this.getActiveCubeFace=function(){return S},this.getActiveMipmapLevel=function(){return b},this.getRenderTarget=function(){return T},this.setRenderTargetTextures=function(t,e,n){tt.get(t.texture).__webglTexture=e,tt.get(t.depthTexture).__webglTexture=n;const i=tt.get(t);i.__hasExternalTextures=!0,i.__autoAllocateDepthBuffer=void 0===n,i.__autoAllocateDepthBuffer||!0===J.has("WEBGL_multisampled_render_to_texture")&&(console.warn("THREE.WebGLRenderer: Render-to-texture extension was disabled because an external texture was provided"),i.__useRenderToTexture=!1)},this.setRenderTargetFramebuffer=function(t,e){const n=tt.get(t);n.__webglFramebuffer=e,n.__useDefaultFramebuffer=void 0===e},this.setRenderTarget=function(t,e=0,n=0){T=t,S=e,b=n;let i=!0,r=null,s=!1,a=!1;if(t){const o=tt.get(t);if(void 0!==o.__useDefaultFramebuffer)$.bindFramebuffer(yt.FRAMEBUFFER,null),i=!1;else if(void 0===o.__webglFramebuffer)et.setupRenderTarget(t);else if(o.__hasExternalTextures)et.rebindTextures(t,tt.get(t.texture).__webglTexture,tt.get(t.depthTexture).__webglTexture);else if(t.depthBuffer){const e=t.depthTexture;if(o.__boundDepthTexture!==e){if(null!==e&&tt.has(e)&&(t.width!==e.image.width||t.height!==e.image.height))throw new Error("WebGLRenderTarget: Attached DepthTexture is initialized to the incorrect size.");et.setupDepthRenderbuffer(t)}}const l=t.texture;(l.isData3DTexture||l.isDataArrayTexture||l.isCompressedArrayTexture)&&(a=!0);const c=tt.get(t).__webglFramebuffer;t.isWebGLCubeRenderTarget?(r=Array.isArray(c[e])?c[e][n]:c[e],s=!0):r=t.samples>0&&!1===et.useMultisampledRTT(t)?tt.get(t).__webglMultisampledFramebuffer:Array.isArray(c)?c[n]:c,A.copy(t.viewport),R.copy(t.scissor),C=t.scissorTest}else A.copy(F).multiplyScalar(D).floor(),R.copy(B).multiplyScalar(D).floor(),C=z;if($.bindFramebuffer(yt.FRAMEBUFFER,r)&&i&&$.drawBuffers(t,r),$.viewport(A),$.scissor(R),$.setScissorTest(C),s){const i=tt.get(t.texture);yt.framebufferTexture2D(yt.FRAMEBUFFER,yt.COLOR_ATTACHMENT0,yt.TEXTURE_CUBE_MAP_POSITIVE_X+e,i.__webglTexture,n)}else if(a){const i=tt.get(t.texture),r=e||0;yt.framebufferTextureLayer(yt.FRAMEBUFFER,yt.COLOR_ATTACHMENT0,i.__webglTexture,n||0,r)}w=-1},this.readRenderTargetPixels=function(t,e,n,i,r,s,a){if(!t||!t.isWebGLRenderTarget)return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let o=tt.get(t).__webglFramebuffer;if(t.isWebGLCubeRenderTarget&&void 0!==a&&(o=o[a]),o){$.bindFramebuffer(yt.FRAMEBUFFER,o);try{const a=t.texture,o=a.format,l=a.type;if(!K.textureFormatReadable(o))return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");if(!K.textureTypeReadable(l))return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");e>=0&&e<=t.width-i&&n>=0&&n<=t.height-r&&yt.readPixels(e,n,i,r,vt.convert(o),vt.convert(l),s)}finally{const t=null!==T?tt.get(T).__webglFramebuffer:null;$.bindFramebuffer(yt.FRAMEBUFFER,t)}}},this.readRenderTargetPixelsAsync=async function(t,e,n,i,r,s,a){if(!t||!t.isWebGLRenderTarget)throw new Error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let o=tt.get(t).__webglFramebuffer;if(t.isWebGLCubeRenderTarget&&void 0!==a&&(o=o[a]),o){const a=t.texture,l=a.format,c=a.type;if(!K.textureFormatReadable(l))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.");if(!K.textureTypeReadable(c))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.");if(e>=0&&e<=t.width-i&&n>=0&&n<=t.height-r){$.bindFramebuffer(yt.FRAMEBUFFER,o);const t=yt.createBuffer();yt.bindBuffer(yt.PIXEL_PACK_BUFFER,t),yt.bufferData(yt.PIXEL_PACK_BUFFER,s.byteLength,yt.STREAM_READ),yt.readPixels(e,n,i,r,vt.convert(l),vt.convert(c),0);const a=null!==T?tt.get(T).__webglFramebuffer:null;$.bindFramebuffer(yt.FRAMEBUFFER,a);const h=yt.fenceSync(yt.SYNC_GPU_COMMANDS_COMPLETE,0);return yt.flush(),await function(t,e,n){return new Promise((function(i,r){setTimeout((function s(){switch(t.clientWaitSync(e,t.SYNC_FLUSH_COMMANDS_BIT,0)){case t.WAIT_FAILED:r();break;case t.TIMEOUT_EXPIRED:setTimeout(s,n);break;default:i()}}),n)}))}(yt,h,4),yt.bindBuffer(yt.PIXEL_PACK_BUFFER,t),yt.getBufferSubData(yt.PIXEL_PACK_BUFFER,0,s),yt.deleteBuffer(t),yt.deleteSync(h),s}throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.")}},this.copyFramebufferToTexture=function(t,e=null,n=0){!0!==t.isTexture&&(si("WebGLRenderer: copyFramebufferToTexture function signature has changed."),e=arguments[0]||null,t=arguments[1]);const i=Math.pow(2,-n),r=Math.floor(t.image.width*i),s=Math.floor(t.image.height*i),a=null!==e?e.x:0,o=null!==e?e.y:0;et.setTexture2D(t,0),yt.copyTexSubImage2D(yt.TEXTURE_2D,n,0,0,a,o,r,s),$.unbindTexture()},this.copyTextureToTexture=function(t,e,n=null,i=null,r=0){let s,a,o,l,c,h,u,d,p;!0!==t.isTexture&&(si("WebGLRenderer: copyTextureToTexture function signature has changed."),i=arguments[0]||null,t=arguments[1],e=arguments[2],r=arguments[3]||0,n=null);const m=t.isCompressedTexture?t.mipmaps[r]:t.image;null!==n?(s=n.max.x-n.min.x,a=n.max.y-n.min.y,o=n.isBox3?n.max.z-n.min.z:1,l=n.min.x,c=n.min.y,h=n.isBox3?n.min.z:0):(s=m.width,a=m.height,o=m.depth||1,l=0,c=0,h=0),null!==i?(u=i.x,d=i.y,p=i.z):(u=0,d=0,p=0);const f=vt.convert(e.format),g=vt.convert(e.type);let v;e.isData3DTexture?(et.setTexture3D(e,0),v=yt.TEXTURE_3D):e.isDataArrayTexture||e.isCompressedArrayTexture?(et.setTexture2DArray(e,0),v=yt.TEXTURE_2D_ARRAY):(et.setTexture2D(e,0),v=yt.TEXTURE_2D),yt.pixelStorei(yt.UNPACK_FLIP_Y_WEBGL,e.flipY),yt.pixelStorei(yt.UNPACK_PREMULTIPLY_ALPHA_WEBGL,e.premultiplyAlpha),yt.pixelStorei(yt.UNPACK_ALIGNMENT,e.unpackAlignment);const _=yt.getParameter(yt.UNPACK_ROW_LENGTH),x=yt.getParameter(yt.UNPACK_IMAGE_HEIGHT),y=yt.getParameter(yt.UNPACK_SKIP_PIXELS),M=yt.getParameter(yt.UNPACK_SKIP_ROWS),S=yt.getParameter(yt.UNPACK_SKIP_IMAGES);yt.pixelStorei(yt.UNPACK_ROW_LENGTH,m.width),yt.pixelStorei(yt.UNPACK_IMAGE_HEIGHT,m.height),yt.pixelStorei(yt.UNPACK_SKIP_PIXELS,l),yt.pixelStorei(yt.UNPACK_SKIP_ROWS,c),yt.pixelStorei(yt.UNPACK_SKIP_IMAGES,h);const b=t.isDataArrayTexture||t.isData3DTexture,T=e.isDataArrayTexture||e.isData3DTexture;if(t.isRenderTargetTexture||t.isDepthTexture){const n=tt.get(t),i=tt.get(e),m=tt.get(n.__renderTarget),f=tt.get(i.__renderTarget);$.bindFramebuffer(yt.READ_FRAMEBUFFER,m.__webglFramebuffer),$.bindFramebuffer(yt.DRAW_FRAMEBUFFER,f.__webglFramebuffer);for(let n=0;n0&&(e.object.backgroundBlurriness=this.backgroundBlurriness),1!==this.backgroundIntensity&&(e.object.backgroundIntensity=this.backgroundIntensity),e.object.backgroundRotation=this.backgroundRotation.toArray(),1!==this.environmentIntensity&&(e.object.environmentIntensity=this.environmentIntensity),e.object.environmentRotation=this.environmentRotation.toArray(),e}}class sc{constructor(t,e){this.isInterleavedBuffer=!0,this.array=t,this.stride=e,this.count=void 0!==t?t.length/e:0,this.usage=wn,this.updateRanges=[],this.version=0,this.uuid=Gn()}onUploadCallback(){}set needsUpdate(t){!0===t&&this.version++}setUsage(t){return this.usage=t,this}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}copy(t){return this.array=new t.array.constructor(t.array),this.count=t.count,this.stride=t.stride,this.usage=t.usage,this}copyAt(t,e,n){t*=this.stride,n*=e.stride;for(let i=0,r=this.stride;it.far||e.push({distance:o,point:hc.clone(),uv:jr.getInterpolation(hc,gc,vc,_c,xc,yc,Mc,new Jn),face:null,object:this})}copy(t,e){return super.copy(t,e),void 0!==t.center&&this.center.copy(t.center),this.material=t.material,this}}function bc(t,e,n,i,r,s){pc.subVectors(t,n).addScalar(.5).multiply(i),void 0!==r?(mc.x=s*pc.x-r*pc.y,mc.y=r*pc.x+s*pc.y):mc.copy(pc),t.copy(e),t.x+=mc.x,t.y+=mc.y,t.applyMatrix4(fc)}const Tc=new Ci,wc=new Ci;class Ec extends Lr{constructor(){super(),this._currentLevel=0,this.type="LOD",Object.defineProperties(this,{levels:{enumerable:!0,value:[]},isLOD:{value:!0}}),this.autoUpdate=!0}copy(t){super.copy(t,!1);const e=t.levels;for(let t=0,n=e.length;t0){let n,i;for(n=1,i=e.length;n0){Tc.setFromMatrixPosition(this.matrixWorld);const n=t.ray.origin.distanceTo(Tc);this.getObjectForDistance(n).raycast(t,e)}}update(t){const e=this.levels;if(e.length>1){Tc.setFromMatrixPosition(t.matrixWorld),wc.setFromMatrixPosition(this.matrixWorld);const n=Tc.distanceTo(wc)/t.zoom;let i,r;for(e[0].object.visible=!0,i=1,r=e.length;i=t))break;e[i-1].object.visible=!1,e[i].object.visible=!0}for(this._currentLevel=i-1;i=r.length&&r.push({start:-1,count:-1,z:-1,index:-1});const a=r[this.index];s.push(a),this.index++,a.start=t,a.count=e,a.z=n,a.index=i}reset(){this.list.length=0,this.index=0}}const eh=new sr,nh=new Kr(1,1,1),ih=new oa,rh=new Li,sh=new Ji,ah=new Ci,oh=new Ci,lh=new Ci,ch=new th,hh=new Bs,uh=[];function dh(t,e,n=0){const i=e.itemSize;if(t.isInterleavedBufferAttribute||t.array.constructor!==e.array.constructor){const r=t.count;for(let s=0;s65535?new Uint32Array(i):new Uint16Array(i);e.setIndex(new cs(t,1))}this._geometryInitialized=!0}}_validateGeometry(t){const e=this.geometry;if(Boolean(t.getIndex())!==Boolean(e.getIndex()))throw new Error('BatchedMesh: All geometries must consistently have "index".');for(const n in e.attributes){if(!t.hasAttribute(n))throw new Error(`BatchedMesh: Added geometry missing "${n}". All geometries must have consistent attributes.`);const i=t.getAttribute(n),r=e.getAttribute(n);if(i.itemSize!==r.itemSize||i.normalized!==r.normalized)throw new Error("BatchedMesh: All attributes must have a consistent itemSize and normalized value.")}}setCustomSort(t){return this.customSort=t,this}computeBoundingBox(){null===this.boundingBox&&(this.boundingBox=new Li);const t=this.boundingBox,e=this._instanceInfo;t.makeEmpty();for(let n=0,i=e.length;n=this.maxInstanceCount&&0===this._availableInstanceIds.length)throw new Error("BatchedMesh: Maximum item count reached.");const e={visible:!0,active:!0,geometryIndex:t};let n=null;this._availableInstanceIds.length>0?(this._availableInstanceIds.sort(Kc),n=this._availableInstanceIds.shift(),this._instanceInfo[n]=e):(n=this._instanceInfo.length,this._instanceInfo.push(e));const i=this._matricesTexture;eh.identity().toArray(i.image.data,16*n),i.needsUpdate=!0;const r=this._colorsTexture;return r&&(nh.toArray(r.image.data,4*n),r.needsUpdate=!0),this._visibilityChanged=!0,n}addGeometry(t,e=-1,n=-1){this._initializeGeometry(t),this._validateGeometry(t);const i={vertexStart:-1,vertexCount:-1,reservedVertexCount:-1,indexStart:-1,indexCount:-1,reservedIndexCount:-1,start:-1,count:-1,boundingBox:null,boundingSphere:null,active:!0},r=this._geometryInfo;i.vertexStart=this._nextVertexStart,i.reservedVertexCount=-1===e?t.getAttribute("position").count:e;const s=t.getIndex();if(null!==s&&(i.indexStart=this._nextIndexStart,i.reservedIndexCount=-1===n?s.count:n),-1!==i.indexStart&&i.indexStart+i.reservedIndexCount>this._maxIndexCount||i.vertexStart+i.reservedVertexCount>this._maxVertexCount)throw new Error("BatchedMesh: Reserved space request exceeds the maximum buffer size.");let a;return this._availableGeometryIds.length>0?(this._availableGeometryIds.sort(Kc),a=this._availableGeometryIds.shift(),r[a]=i):(a=this._geometryCount,this._geometryCount++,r.push(i)),this.setGeometryAt(a,t),this._nextIndexStart=i.indexStart+i.reservedIndexCount,this._nextVertexStart=i.vertexStart+i.reservedVertexCount,a}setGeometryAt(t,e){if(t>=this._geometryCount)throw new Error("BatchedMesh: Maximum geometry count reached.");this._validateGeometry(e);const n=this.geometry,i=null!==n.getIndex(),r=n.getIndex(),s=e.getIndex(),a=this._geometryInfo[t];if(i&&s.count>a.reservedIndexCount||e.attributes.position.count>a.reservedVertexCount)throw new Error("BatchedMesh: Reserved space not large enough for provided geometry.");const o=a.vertexStart,l=a.reservedVertexCount;a.vertexCount=e.getAttribute("position").count;for(const t in n.attributes){const i=e.getAttribute(t),r=n.getAttribute(t);dh(i,r,o);const s=i.itemSize;for(let t=i.count,e=l;t=e.length||!1===e[t].active)return this;const n=this._instanceInfo;for(let e=0,i=n.length;e=e.length||!1===e[t].active||(e[t].active=!1,this._availableInstanceIds.push(t),this._visibilityChanged=!0),this}optimize(){let t=0,e=0;const n=this._geometryInfo,i=n.map(((t,e)=>e)).sort(((t,e)=>n[t].vertexStart-n[e].vertexStart)),r=this.geometry;for(let s=0,a=n.length;s=this._geometryCount)return null;const n=this.geometry,i=this._geometryInfo[t];if(null===i.boundingBox){const t=new Li,e=n.index,r=n.attributes.position;for(let n=i.start,s=i.start+i.count;n=this._geometryCount)return null;const n=this.geometry,i=this._geometryInfo[t];if(null===i.boundingSphere){const e=new Ji;this.getBoundingBoxAt(t,rh),rh.getCenter(e.center);const r=n.index,s=n.attributes.position;let a=0;for(let t=i.start,n=i.start+i.count;t=n.length||!1===n[t].active||(e.toArray(r,16*t),i.needsUpdate=!0),this}getMatrixAt(t,e){const n=this._instanceInfo,i=this._matricesTexture.image.data;return t>=n.length||!1===n[t].active?null:e.fromArray(i,16*t)}setColorAt(t,e){null===this._colorsTexture&&this._initColorsTexture();const n=this._colorsTexture,i=this._colorsTexture.image.data,r=this._instanceInfo;return t>=r.length||!1===r[t].active||(e.toArray(i,4*t),n.needsUpdate=!0),this}getColorAt(t,e){const n=this._colorsTexture.image.data,i=this._instanceInfo;return t>=i.length||!1===i[t].active?null:e.fromArray(n,4*t)}setVisibleAt(t,e){const n=this._instanceInfo;return t>=n.length||!1===n[t].active||n[t].visible===e||(n[t].visible=e,this._visibilityChanged=!0),this}getVisibleAt(t){const e=this._instanceInfo;return!(t>=e.length||!1===e[t].active)&&e[t].visible}setGeometryIdAt(t,e){const n=this._instanceInfo,i=this._geometryInfo;return t>=n.length||!1===n[t].active||e>=i.length||!1===i[e].active?null:(n[t].geometryIndex=e,this)}getGeometryIdAt(t){const e=this._instanceInfo;return t>=e.length||!1===e[t].active?-1:e[t].geometryIndex}getGeometryRangeAt(t,e={}){if(t<0||t>=this._geometryCount)return null;const n=this._geometryInfo[t];return e.vertexStart=n.vertexStart,e.vertexCount=n.vertexCount,e.reservedVertexCount=n.reservedVertexCount,e.indexStart=n.indexStart,e.indexCount=n.indexCount,e.reservedIndexCount=n.reservedIndexCount,e.start=n.start,e.count=n.count,e}setInstanceCount(t){const e=this._availableInstanceIds,n=this._instanceInfo;for(e.sort(Kc);e[e.length-1]===n.length;)n.pop(),e.pop();if(tt.active));if(Math.max(...n.map((t=>t.vertexStart+t.reservedVertexCount)))>t)throw new Error(`BatchedMesh: Geometry vertex values are being used outside the range ${e}. Cannot shrink further.`);if(this.geometry.index){if(Math.max(...n.map((t=>t.indexStart+t.reservedIndexCount)))>e)throw new Error(`BatchedMesh: Geometry index values are being used outside the range ${e}. Cannot shrink further.`)}const i=this.geometry;i.dispose(),this._maxVertexCount=t,this._maxIndexCount=e,this._geometryInitialized&&(this._geometryInitialized=!1,this.geometry=new Es,this._initializeGeometry(i));const r=this.geometry;i.index&&ph(i.index.array,r.index.array);for(const t in i.attributes)ph(i.attributes[t].array,r.attributes[t].array)}raycast(t,e){const n=this._instanceInfo,i=this._geometryInfo,r=this.matrixWorld,s=this.geometry;hh.material=this.material,hh.geometry.index=s.index,hh.geometry.attributes=s.attributes,null===hh.geometry.boundingBox&&(hh.geometry.boundingBox=new Li),null===hh.geometry.boundingSphere&&(hh.geometry.boundingSphere=new Ji);for(let s=0,a=n.length;s({...t,boundingBox:null!==t.boundingBox?t.boundingBox.clone():null,boundingSphere:null!==t.boundingSphere?t.boundingSphere.clone():null}))),this._instanceInfo=t._instanceInfo.map((t=>({...t}))),this._maxInstanceCount=t._maxInstanceCount,this._maxVertexCount=t._maxVertexCount,this._maxIndexCount=t._maxIndexCount,this._geometryInitialized=t._geometryInitialized,this._geometryCount=t._geometryCount,this._multiDrawCounts=t._multiDrawCounts.slice(),this._multiDrawStarts=t._multiDrawStarts.slice(),this._matricesTexture=t._matricesTexture.clone(),this._matricesTexture.image.data=this._matricesTexture.image.data.slice(),null!==this._colorsTexture&&(this._colorsTexture=t._colorsTexture.clone(),this._colorsTexture.image.data=this._colorsTexture.image.data.slice()),this}dispose(){return this.geometry.dispose(),this._matricesTexture.dispose(),this._matricesTexture=null,this._indirectTexture.dispose(),this._indirectTexture=null,null!==this._colorsTexture&&(this._colorsTexture.dispose(),this._colorsTexture=null),this}onBeforeRender(t,e,n,i,r){if(!this._visibilityChanged&&!this.perObjectFrustumCulled&&!this.sortObjects)return;const s=i.getIndex(),a=null===s?1:s.array.BYTES_PER_ELEMENT,o=this._instanceInfo,l=this._multiDrawStarts,c=this._multiDrawCounts,h=this._geometryInfo,u=this.perObjectFrustumCulled,d=this._indirectTexture,p=d.image.data;u&&(eh.multiplyMatrices(n.projectionMatrix,n.matrixWorldInverse).multiply(this.matrixWorld),ih.setFromProjectionMatrix(eh,t.coordinateSystem));let m=0;if(this.sortObjects){eh.copy(this.matrixWorld).invert(),ah.setFromMatrixPosition(n.matrixWorld).applyMatrix4(eh),oh.set(0,0,-1).transformDirection(n.matrixWorld).transformDirection(eh);for(let t=0,e=o.length;t0){const n=t[e[0]];if(void 0!==n){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=n.length;ti)return;Mh.applyMatrix4(t.matrixWorld);const o=e.ray.origin.distanceTo(Mh);return oe.far?void 0:{distance:o,point:Sh.clone().applyMatrix4(t.matrixWorld),index:r,face:null,faceIndex:null,barycoord:null,object:t}}const wh=new Ci,Eh=new Ci;class Ah extends bh{constructor(t,e){super(t,e),this.isLineSegments=!0,this.type="LineSegments"}computeLineDistances(){const t=this.geometry;if(null===t.index){const e=t.attributes.position,n=[];for(let t=0,i=e.count;t0){const n=t[e[0]];if(void 0!==n){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=n.length;tr.far)return;s.push({distance:l,distanceToRay:Math.sqrt(o),point:n,index:e,face:null,faceIndex:null,barycoord:null,object:a})}}class Oh extends yi{constructor(t,e,n,i,r,s,a,o,l){super(t,e,n,i,r,s,a,o,l),this.isVideoTexture=!0,this.minFilter=void 0!==s?s:Mt,this.magFilter=void 0!==r?r:Mt,this.generateMipmaps=!1;const c=this;"requestVideoFrameCallback"in t&&t.requestVideoFrameCallback((function e(){c.needsUpdate=!0,t.requestVideoFrameCallback(e)}))}clone(){return new this.constructor(this.image).copy(this)}update(){const t=this.image;!1==="requestVideoFrameCallback"in t&&t.readyState>=t.HAVE_CURRENT_DATA&&(this.needsUpdate=!0)}}class Fh extends yi{constructor(t,e){super({width:t,height:e}),this.isFramebufferTexture=!0,this.magFilter=gt,this.minFilter=gt,this.generateMipmaps=!1,this.needsUpdate=!0}}class Bh extends yi{constructor(t,e,n,i,r,s,a,o,l,c,h,u){super(null,s,a,o,l,c,i,r,h,u),this.isCompressedTexture=!0,this.image={width:e,height:n},this.mipmaps=t,this.flipY=!1,this.generateMipmaps=!1}}class zh extends Bh{constructor(t,e,n,i,r,s){super(t,e,n,r,s),this.isCompressedArrayTexture=!0,this.image.depth=i,this.wrapR=mt,this.layerUpdates=new Set}addLayerUpdate(t){this.layerUpdates.add(t)}clearLayerUpdates(){this.layerUpdates.clear()}}class kh extends Bh{constructor(t,e,n){super(void 0,t[0].width,t[0].height,e,n,lt),this.isCompressedCubeTexture=!0,this.isCubeTexture=!0,this.image=t}}class Vh extends yi{constructor(t,e,n,i,r,s,a,o,l){super(t,e,n,i,r,s,a,o,l),this.isCanvasTexture=!0,this.needsUpdate=!0}}class Hh{constructor(){this.type="Curve",this.arcLengthDivisions=200}getPoint(){return console.warn("THREE.Curve: .getPoint() not implemented."),null}getPointAt(t,e){const n=this.getUtoTmapping(t);return this.getPoint(n,e)}getPoints(t=5){const e=[];for(let n=0;n<=t;n++)e.push(this.getPoint(n/t));return e}getSpacedPoints(t=5){const e=[];for(let n=0;n<=t;n++)e.push(this.getPointAt(n/t));return e}getLength(){const t=this.getLengths();return t[t.length-1]}getLengths(t=this.arcLengthDivisions){if(this.cacheArcLengths&&this.cacheArcLengths.length===t+1&&!this.needsUpdate)return this.cacheArcLengths;this.needsUpdate=!1;const e=[];let n,i=this.getPoint(0),r=0;e.push(0);for(let s=1;s<=t;s++)n=this.getPoint(s/t),r+=n.distanceTo(i),e.push(r),i=n;return this.cacheArcLengths=e,e}updateArcLengths(){this.needsUpdate=!0,this.getLengths()}getUtoTmapping(t,e){const n=this.getLengths();let i=0;const r=n.length;let s;s=e||t*n[r-1];let a,o=0,l=r-1;for(;o<=l;)if(i=Math.floor(o+(l-o)/2),a=n[i]-s,a<0)o=i+1;else{if(!(a>0)){l=i;break}l=i-1}if(i=l,n[i]===s)return i/(r-1);const c=n[i];return(i+(s-c)/(n[i+1]-c))/(r-1)}getTangent(t,e){const n=1e-4;let i=t-n,r=t+n;i<0&&(i=0),r>1&&(r=1);const s=this.getPoint(i),a=this.getPoint(r),o=e||(s.isVector2?new Jn:new Ci);return o.copy(a).sub(s).normalize(),o}getTangentAt(t,e){const n=this.getUtoTmapping(t);return this.getTangent(n,e)}computeFrenetFrames(t,e){const n=new Ci,i=[],r=[],s=[],a=new Ci,o=new sr;for(let e=0;e<=t;e++){const n=e/t;i[e]=this.getTangentAt(n,new Ci)}r[0]=new Ci,s[0]=new Ci;let l=Number.MAX_VALUE;const c=Math.abs(i[0].x),h=Math.abs(i[0].y),u=Math.abs(i[0].z);c<=l&&(l=c,n.set(1,0,0)),h<=l&&(l=h,n.set(0,1,0)),u<=l&&n.set(0,0,1),a.crossVectors(i[0],n).normalize(),r[0].crossVectors(i[0],a),s[0].crossVectors(i[0],r[0]);for(let e=1;e<=t;e++){if(r[e]=r[e-1].clone(),s[e]=s[e-1].clone(),a.crossVectors(i[e-1],i[e]),a.length()>Number.EPSILON){a.normalize();const t=Math.acos(Wn(i[e-1].dot(i[e]),-1,1));r[e].applyMatrix4(o.makeRotationAxis(a,t))}s[e].crossVectors(i[e],r[e])}if(!0===e){let e=Math.acos(Wn(r[0].dot(r[t]),-1,1));e/=t,i[0].dot(a.crossVectors(r[0],r[t]))>0&&(e=-e);for(let n=1;n<=t;n++)r[n].applyMatrix4(o.makeRotationAxis(i[n],e*n)),s[n].crossVectors(i[n],r[n])}return{tangents:i,normals:r,binormals:s}}clone(){return(new this.constructor).copy(this)}copy(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}toJSON(){const t={metadata:{version:4.6,type:"Curve",generator:"Curve.toJSON"}};return t.arcLengthDivisions=this.arcLengthDivisions,t.type=this.type,t}fromJSON(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}}class Gh extends Hh{constructor(t=0,e=0,n=1,i=1,r=0,s=2*Math.PI,a=!1,o=0){super(),this.isEllipseCurve=!0,this.type="EllipseCurve",this.aX=t,this.aY=e,this.xRadius=n,this.yRadius=i,this.aStartAngle=r,this.aEndAngle=s,this.aClockwise=a,this.aRotation=o}getPoint(t,e=new Jn){const n=e,i=2*Math.PI;let r=this.aEndAngle-this.aStartAngle;const s=Math.abs(r)i;)r-=i;r0?0:(Math.floor(Math.abs(l)/r)+1)*r:0===c&&l===r-1&&(l=r-2,c=1),this.closed||l>0?a=i[(l-1)%r]:(jh.subVectors(i[0],i[1]).add(i[0]),a=jh);const h=i[l%r],u=i[(l+1)%r];if(this.closed||l+2i.length-2?i.length-1:s+1],h=i[s>i.length-3?i.length-1:s+2];return n.set(Kh(a,o.x,l.x,c.x,h.x),Kh(a,o.y,l.y,c.y,h.y)),n}copy(t){super.copy(t),this.points=[];for(let e=0,n=t.points.length;e=n){const t=i[r]-n,s=this.curves[r],a=s.getLength(),o=0===a?0:1-t/a;return s.getPointAt(o,e)}r++}return null}getLength(){const t=this.getCurveLengths();return t[t.length-1]}updateArcLengths(){this.needsUpdate=!0,this.cacheLengths=null,this.getCurveLengths()}getCurveLengths(){if(this.cacheLengths&&this.cacheLengths.length===this.curves.length)return this.cacheLengths;const t=[];let e=0;for(let n=0,i=this.curves.length;n1&&!e[e.length-1].equals(e[0])&&e.push(e[0]),e}copy(t){super.copy(t),this.curves=[];for(let e=0,n=t.curves.length;e0){const t=l.getPoint(0);t.equals(this.currentPoint)||this.lineTo(t.x,t.y)}this.curves.push(l);const c=l.getPoint(1);return this.currentPoint.copy(c),this}copy(t){return super.copy(t),this.currentPoint.copy(t.currentPoint),this}toJSON(){const t=super.toJSON();return t.currentPoint=this.currentPoint.toArray(),t}fromJSON(t){return super.fromJSON(t),this.currentPoint.fromArray(t.currentPoint),this}}class hu extends Es{constructor(t=[new Jn(0,-.5),new Jn(.5,0),new Jn(0,.5)],e=12,n=0,i=2*Math.PI){super(),this.type="LatheGeometry",this.parameters={points:t,segments:e,phiStart:n,phiLength:i},e=Math.floor(e),i=Wn(i,0,2*Math.PI);const r=[],s=[],a=[],o=[],l=[],c=1/e,h=new Ci,u=new Jn,d=new Ci,p=new Ci,m=new Ci;let f=0,g=0;for(let e=0;e<=t.length-1;e++)switch(e){case 0:f=t[e+1].x-t[e].x,g=t[e+1].y-t[e].y,d.x=1*g,d.y=-f,d.z=0*g,m.copy(d),d.normalize(),o.push(d.x,d.y,d.z);break;case t.length-1:o.push(m.x,m.y,m.z);break;default:f=t[e+1].x-t[e].x,g=t[e+1].y-t[e].y,d.x=1*g,d.y=-f,d.z=0*g,p.copy(d),d.x+=m.x,d.y+=m.y,d.z+=m.z,d.normalize(),o.push(d.x,d.y,d.z),m.copy(p)}for(let r=0;r<=e;r++){const d=n+r*c*i,p=Math.sin(d),m=Math.cos(d);for(let n=0;n<=t.length-1;n++){h.x=t[n].x*p,h.y=t[n].y,h.z=t[n].x*m,s.push(h.x,h.y,h.z),u.x=r/e,u.y=n/(t.length-1),a.push(u.x,u.y);const i=o[3*n+0]*p,c=o[3*n+1],d=o[3*n+0]*m;l.push(i,c,d)}}for(let n=0;n0||0!==i)&&(c.push(s,a,l),_+=3),(e>0||i!==r-1)&&(c.push(a,o,l),_+=3)}l.addGroup(g,_,0),g+=_}(),!1===s&&(t>0&&v(!0),e>0&&v(!1)),this.setIndex(c),this.setAttribute("position",new _s(h,3)),this.setAttribute("normal",new _s(u,3)),this.setAttribute("uv",new _s(d,2))}copy(t){return super.copy(t),this.parameters=Object.assign({},t.parameters),this}static fromJSON(t){return new pu(t.radiusTop,t.radiusBottom,t.height,t.radialSegments,t.heightSegments,t.openEnded,t.thetaStart,t.thetaLength)}}class mu extends pu{constructor(t=1,e=1,n=32,i=1,r=!1,s=0,a=2*Math.PI){super(0,t,e,n,i,r,s,a),this.type="ConeGeometry",this.parameters={radius:t,height:e,radialSegments:n,heightSegments:i,openEnded:r,thetaStart:s,thetaLength:a}}static fromJSON(t){return new mu(t.radius,t.height,t.radialSegments,t.heightSegments,t.openEnded,t.thetaStart,t.thetaLength)}}class fu extends Es{constructor(t=[],e=[],n=1,i=0){super(),this.type="PolyhedronGeometry",this.parameters={vertices:t,indices:e,radius:n,detail:i};const r=[],s=[];function a(t,e,n,i){const r=i+1,s=[];for(let i=0;i<=r;i++){s[i]=[];const a=t.clone().lerp(n,i/r),o=e.clone().lerp(n,i/r),l=r-i;for(let t=0;t<=l;t++)s[i][t]=0===t&&i===r?a:a.clone().lerp(o,t/l)}for(let t=0;t.9&&a<.1&&(e<.2&&(s[t+0]+=1),n<.2&&(s[t+2]+=1),i<.2&&(s[t+4]+=1))}}()}(),this.setAttribute("position",new _s(r,3)),this.setAttribute("normal",new _s(r.slice(),3)),this.setAttribute("uv",new _s(s,2)),0===i?this.computeVertexNormals():this.normalizeNormals()}copy(t){return super.copy(t),this.parameters=Object.assign({},t.parameters),this}static fromJSON(t){return new fu(t.vertices,t.indices,t.radius,t.details)}}class gu extends fu{constructor(t=1,e=0){const n=(1+Math.sqrt(5))/2,i=1/n;super([-1,-1,-1,-1,-1,1,-1,1,-1,-1,1,1,1,-1,-1,1,-1,1,1,1,-1,1,1,1,0,-i,-n,0,-i,n,0,i,-n,0,i,n,-i,-n,0,-i,n,0,i,-n,0,i,n,0,-n,0,-i,n,0,-i,-n,0,i,n,0,i],[3,11,7,3,7,15,3,15,13,7,19,17,7,17,6,7,6,15,17,4,8,17,8,10,17,10,6,8,0,16,8,16,2,8,2,10,0,12,1,0,1,18,0,18,16,6,10,2,6,2,13,6,13,15,2,16,18,2,18,3,2,3,13,18,1,9,18,9,11,18,11,3,4,14,12,4,12,0,4,0,8,11,9,5,11,5,19,11,19,7,19,5,14,19,14,4,19,4,17,1,12,14,1,14,5,1,5,9],t,e),this.type="DodecahedronGeometry",this.parameters={radius:t,detail:e}}static fromJSON(t){return new gu(t.radius,t.detail)}}const vu=new Ci,_u=new Ci,xu=new Ci,yu=new jr;class Mu extends Es{constructor(t=null,e=1){if(super(),this.type="EdgesGeometry",this.parameters={geometry:t,thresholdAngle:e},null!==t){const n=4,i=Math.pow(10,n),r=Math.cos(Vn*e),s=t.getIndex(),a=t.getAttribute("position"),o=s?s.count:a.count,l=[0,0,0],c=["a","b","c"],h=new Array(3),u={},d=[];for(let t=0;t80*n){o=c=t[0],l=h=t[1];for(let e=n;ec&&(c=u),d>h&&(h=d);p=Math.max(c-o,h-l),p=0!==p?32767/p:0}return Eu(s,a,n,o,l,p,0),a};function Tu(t,e,n,i,r){let s,a;if(r===function(t,e,n,i){let r=0;for(let s=e,a=n-i;s0)for(s=e;s=e;s-=i)a=Xu(s,t[s],t[s+1],a);return a&&zu(a,a.next)&&(ju(a),a=a.next),a}function wu(t,e){if(!t)return t;e||(e=t);let n,i=t;do{if(n=!1,i.steiner||!zu(i,i.next)&&0!==Bu(i.prev,i,i.next))i=i.next;else{if(ju(i),i=e=i.prev,i===i.next)break;n=!0}}while(n||i!==e);return e}function Eu(t,e,n,i,r,s,a){if(!t)return;!a&&s&&function(t,e,n,i){let r=t;do{0===r.z&&(r.z=Du(r.x,r.y,e,n,i)),r.prevZ=r.prev,r.nextZ=r.next,r=r.next}while(r!==t);r.prevZ.nextZ=null,r.prevZ=null,function(t){let e,n,i,r,s,a,o,l,c=1;do{for(n=t,t=null,s=null,a=0;n;){for(a++,i=n,o=0,e=0;e0||l>0&&i;)0!==o&&(0===l||!i||n.z<=i.z)?(r=n,n=n.nextZ,o--):(r=i,i=i.nextZ,l--),s?s.nextZ=r:t=r,r.prevZ=s,s=r;n=i}s.nextZ=null,c*=2}while(a>1)}(r)}(t,i,r,s);let o,l,c=t;for(;t.prev!==t.next;)if(o=t.prev,l=t.next,s?Ru(t,i,r,s):Au(t))e.push(o.i/n|0),e.push(t.i/n|0),e.push(l.i/n|0),ju(t),t=l.next,c=l.next;else if((t=l)===c){a?1===a?Eu(t=Cu(wu(t),e,n),e,n,i,r,s,2):2===a&&Iu(t,e,n,i,r,s):Eu(wu(t),e,n,i,r,s,1);break}}function Au(t){const e=t.prev,n=t,i=t.next;if(Bu(e,n,i)>=0)return!1;const r=e.x,s=n.x,a=i.x,o=e.y,l=n.y,c=i.y,h=rs?r>a?r:a:s>a?s:a,p=o>l?o>c?o:c:l>c?l:c;let m=i.next;for(;m!==e;){if(m.x>=h&&m.x<=d&&m.y>=u&&m.y<=p&&Ou(r,o,s,l,a,c,m.x,m.y)&&Bu(m.prev,m,m.next)>=0)return!1;m=m.next}return!0}function Ru(t,e,n,i){const r=t.prev,s=t,a=t.next;if(Bu(r,s,a)>=0)return!1;const o=r.x,l=s.x,c=a.x,h=r.y,u=s.y,d=a.y,p=ol?o>c?o:c:l>c?l:c,g=h>u?h>d?h:d:u>d?u:d,v=Du(p,m,e,n,i),_=Du(f,g,e,n,i);let x=t.prevZ,y=t.nextZ;for(;x&&x.z>=v&&y&&y.z<=_;){if(x.x>=p&&x.x<=f&&x.y>=m&&x.y<=g&&x!==r&&x!==a&&Ou(o,h,l,u,c,d,x.x,x.y)&&Bu(x.prev,x,x.next)>=0)return!1;if(x=x.prevZ,y.x>=p&&y.x<=f&&y.y>=m&&y.y<=g&&y!==r&&y!==a&&Ou(o,h,l,u,c,d,y.x,y.y)&&Bu(y.prev,y,y.next)>=0)return!1;y=y.nextZ}for(;x&&x.z>=v;){if(x.x>=p&&x.x<=f&&x.y>=m&&x.y<=g&&x!==r&&x!==a&&Ou(o,h,l,u,c,d,x.x,x.y)&&Bu(x.prev,x,x.next)>=0)return!1;x=x.prevZ}for(;y&&y.z<=_;){if(y.x>=p&&y.x<=f&&y.y>=m&&y.y<=g&&y!==r&&y!==a&&Ou(o,h,l,u,c,d,y.x,y.y)&&Bu(y.prev,y,y.next)>=0)return!1;y=y.nextZ}return!0}function Cu(t,e,n){let i=t;do{const r=i.prev,s=i.next.next;!zu(r,s)&&ku(r,i,i.next,s)&&Gu(r,s)&&Gu(s,r)&&(e.push(r.i/n|0),e.push(i.i/n|0),e.push(s.i/n|0),ju(i),ju(i.next),i=t=s),i=i.next}while(i!==t);return wu(i)}function Iu(t,e,n,i,r,s){let a=t;do{let t=a.next.next;for(;t!==a.prev;){if(a.i!==t.i&&Fu(a,t)){let o=Wu(a,t);return a=wu(a,a.next),o=wu(o,o.next),Eu(a,e,n,i,r,s,0),void Eu(o,e,n,i,r,s,0)}t=t.next}a=a.next}while(a!==t)}function Pu(t,e){return t.x-e.x}function Lu(t,e){const n=function(t,e){let n,i=e,r=-1/0;const s=t.x,a=t.y;do{if(a<=i.y&&a>=i.next.y&&i.next.y!==i.y){const t=i.x+(a-i.y)*(i.next.x-i.x)/(i.next.y-i.y);if(t<=s&&t>r&&(r=t,n=i.x=i.x&&i.x>=l&&s!==i.x&&Ou(an.x||i.x===n.x&&Uu(n,i)))&&(n=i,u=h)),i=i.next}while(i!==o);return n}(t,e);if(!n)return e;const i=Wu(n,t);return wu(i,i.next),wu(n,n.next)}function Uu(t,e){return Bu(t.prev,t,e.prev)<0&&Bu(e.next,t,t.next)<0}function Du(t,e,n,i,r){return(t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=(t-n)*r|0)|t<<8))|t<<4))|t<<2))|t<<1))|(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e=(e-i)*r|0)|e<<8))|e<<4))|e<<2))|e<<1))<<1}function Nu(t){let e=t,n=t;do{(e.x=(t-a)*(s-o)&&(t-a)*(i-o)>=(n-a)*(e-o)&&(n-a)*(s-o)>=(r-a)*(i-o)}function Fu(t,e){return t.next.i!==e.i&&t.prev.i!==e.i&&!function(t,e){let n=t;do{if(n.i!==t.i&&n.next.i!==t.i&&n.i!==e.i&&n.next.i!==e.i&&ku(n,n.next,t,e))return!0;n=n.next}while(n!==t);return!1}(t,e)&&(Gu(t,e)&&Gu(e,t)&&function(t,e){let n=t,i=!1;const r=(t.x+e.x)/2,s=(t.y+e.y)/2;do{n.y>s!=n.next.y>s&&n.next.y!==n.y&&r<(n.next.x-n.x)*(s-n.y)/(n.next.y-n.y)+n.x&&(i=!i),n=n.next}while(n!==t);return i}(t,e)&&(Bu(t.prev,t,e.prev)||Bu(t,e.prev,e))||zu(t,e)&&Bu(t.prev,t,t.next)>0&&Bu(e.prev,e,e.next)>0)}function Bu(t,e,n){return(e.y-t.y)*(n.x-e.x)-(e.x-t.x)*(n.y-e.y)}function zu(t,e){return t.x===e.x&&t.y===e.y}function ku(t,e,n,i){const r=Hu(Bu(t,e,n)),s=Hu(Bu(t,e,i)),a=Hu(Bu(n,i,t)),o=Hu(Bu(n,i,e));return r!==s&&a!==o||(!(0!==r||!Vu(t,n,e))||(!(0!==s||!Vu(t,i,e))||(!(0!==a||!Vu(n,t,i))||!(0!==o||!Vu(n,e,i)))))}function Vu(t,e,n){return e.x<=Math.max(t.x,n.x)&&e.x>=Math.min(t.x,n.x)&&e.y<=Math.max(t.y,n.y)&&e.y>=Math.min(t.y,n.y)}function Hu(t){return t>0?1:t<0?-1:0}function Gu(t,e){return Bu(t.prev,t,t.next)<0?Bu(t,e,t.next)>=0&&Bu(t,t.prev,e)>=0:Bu(t,e,t.prev)<0||Bu(t,t.next,e)<0}function Wu(t,e){const n=new qu(t.i,t.x,t.y),i=new qu(e.i,e.x,e.y),r=t.next,s=e.prev;return t.next=e,e.prev=t,n.next=r,r.prev=n,i.next=n,n.prev=i,s.next=i,i.prev=s,i}function Xu(t,e,n,i){const r=new qu(t,e,n);return i?(r.next=i.next,r.prev=i,i.next.prev=r,i.next=r):(r.prev=r,r.next=r),r}function ju(t){t.next.prev=t.prev,t.prev.next=t.next,t.prevZ&&(t.prevZ.nextZ=t.nextZ),t.nextZ&&(t.nextZ.prevZ=t.prevZ)}function qu(t,e,n){this.i=t,this.x=e,this.y=n,this.prev=null,this.next=null,this.z=0,this.prevZ=null,this.nextZ=null,this.steiner=!1}class Yu{static area(t){const e=t.length;let n=0;for(let i=e-1,r=0;r2&&t[e-1].equals(t[0])&&t.pop()}function Ju(t,e){for(let n=0;nNumber.EPSILON){const u=Math.sqrt(h),d=Math.sqrt(l*l+c*c),p=e.x-o/u,m=e.y+a/u,f=((n.x-c/d-p)*c-(n.y+l/d-m)*l)/(a*c-o*l);i=p+a*f-t.x,r=m+o*f-t.y;const g=i*i+r*r;if(g<=2)return new Jn(i,r);s=Math.sqrt(g/2)}else{let t=!1;a>Number.EPSILON?l>Number.EPSILON&&(t=!0):a<-Number.EPSILON?l<-Number.EPSILON&&(t=!0):Math.sign(o)===Math.sign(c)&&(t=!0),t?(i=-o,r=a,s=Math.sqrt(h)):(i=a,r=o,s=Math.sqrt(h/2))}return new Jn(i/s,r/s)}const P=[];for(let t=0,e=E.length,n=e-1,i=t+1;t=0;t--){const e=t/p,n=h*Math.cos(e*Math.PI/2),i=u*Math.sin(e*Math.PI/2)+d;for(let t=0,e=E.length;t=0;){const i=n;let r=n-1;r<0&&(r=t.length-1);for(let t=0,n=o+2*p;t0)&&d.push(e,r,l),(t!==n-1||o0!=t>0&&this.version++,this._anisotropy=t}get clearcoat(){return this._clearcoat}set clearcoat(t){this._clearcoat>0!=t>0&&this.version++,this._clearcoat=t}get iridescence(){return this._iridescence}set iridescence(t){this._iridescence>0!=t>0&&this.version++,this._iridescence=t}get dispersion(){return this._dispersion}set dispersion(t){this._dispersion>0!=t>0&&this.version++,this._dispersion=t}get sheen(){return this._sheen}set sheen(t){this._sheen>0!=t>0&&this.version++,this._sheen=t}get transmission(){return this._transmission}set transmission(t){this._transmission>0!=t>0&&this.version++,this._transmission=t}copy(t){return super.copy(t),this.defines={STANDARD:"",PHYSICAL:""},this.anisotropy=t.anisotropy,this.anisotropyRotation=t.anisotropyRotation,this.anisotropyMap=t.anisotropyMap,this.clearcoat=t.clearcoat,this.clearcoatMap=t.clearcoatMap,this.clearcoatRoughness=t.clearcoatRoughness,this.clearcoatRoughnessMap=t.clearcoatRoughnessMap,this.clearcoatNormalMap=t.clearcoatNormalMap,this.clearcoatNormalScale.copy(t.clearcoatNormalScale),this.dispersion=t.dispersion,this.ior=t.ior,this.iridescence=t.iridescence,this.iridescenceMap=t.iridescenceMap,this.iridescenceIOR=t.iridescenceIOR,this.iridescenceThicknessRange=[...t.iridescenceThicknessRange],this.iridescenceThicknessMap=t.iridescenceThicknessMap,this.sheen=t.sheen,this.sheenColor.copy(t.sheenColor),this.sheenColorMap=t.sheenColorMap,this.sheenRoughness=t.sheenRoughness,this.sheenRoughnessMap=t.sheenRoughnessMap,this.transmission=t.transmission,this.transmissionMap=t.transmissionMap,this.thickness=t.thickness,this.thicknessMap=t.thicknessMap,this.attenuationDistance=t.attenuationDistance,this.attenuationColor.copy(t.attenuationColor),this.specularIntensity=t.specularIntensity,this.specularIntensityMap=t.specularIntensityMap,this.specularColor.copy(t.specularColor),this.specularColorMap=t.specularColorMap,this}}class fd extends ts{static get type(){return"MeshPhongMaterial"}constructor(t){super(),this.isMeshPhongMaterial=!0,this.color=new Kr(16777215),this.specular=new Kr(1118481),this.shininess=30,this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Kr(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Jn(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new fr,this.combine=0,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.specular.copy(t.specular),this.shininess=t.shininess,this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.flatShading=t.flatShading,this.fog=t.fog,this}}class gd extends ts{static get type(){return"MeshToonMaterial"}constructor(t){super(),this.isMeshToonMaterial=!0,this.defines={TOON:""},this.color=new Kr(16777215),this.map=null,this.gradientMap=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Kr(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Jn(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.gradientMap=t.gradientMap,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.alphaMap=t.alphaMap,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.fog=t.fog,this}}class vd extends ts{static get type(){return"MeshNormalMaterial"}constructor(t){super(),this.isMeshNormalMaterial=!0,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Jn(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.flatShading=!1,this.setValues(t)}copy(t){return super.copy(t),this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.flatShading=t.flatShading,this}}class _d extends ts{static get type(){return"MeshLambertMaterial"}constructor(t){super(),this.isMeshLambertMaterial=!0,this.color=new Kr(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Kr(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Jn(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new fr,this.combine=0,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.flatShading=t.flatShading,this.fog=t.fog,this}}class xd extends ts{static get type(){return"MeshMatcapMaterial"}constructor(t){super(),this.isMeshMatcapMaterial=!0,this.defines={MATCAP:""},this.color=new Kr(16777215),this.matcap=null,this.map=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Jn(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.defines={MATCAP:""},this.color.copy(t.color),this.matcap=t.matcap,this.map=t.map,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.alphaMap=t.alphaMap,this.flatShading=t.flatShading,this.fog=t.fog,this}}class yd extends fh{static get type(){return"LineDashedMaterial"}constructor(t){super(),this.isLineDashedMaterial=!0,this.scale=1,this.dashSize=3,this.gapSize=1,this.setValues(t)}copy(t){return super.copy(t),this.scale=t.scale,this.dashSize=t.dashSize,this.gapSize=t.gapSize,this}}function Md(t,e,n){return!t||!n&&t.constructor===e?t:"number"==typeof e.BYTES_PER_ELEMENT?new e(t):Array.prototype.slice.call(t)}function Sd(t){return ArrayBuffer.isView(t)&&!(t instanceof DataView)}function bd(t){const e=t.length,n=new Array(e);for(let t=0;t!==e;++t)n[t]=t;return n.sort((function(e,n){return t[e]-t[n]})),n}function Td(t,e,n){const i=t.length,r=new t.constructor(i);for(let s=0,a=0;a!==i;++s){const i=n[s]*e;for(let n=0;n!==e;++n)r[a++]=t[i+n]}return r}function wd(t,e,n,i){let r=1,s=t[0];for(;void 0!==s&&void 0===s[i];)s=t[r++];if(void 0===s)return;let a=s[i];if(void 0!==a)if(Array.isArray(a))do{a=s[i],void 0!==a&&(e.push(s.time),n.push.apply(n,a)),s=t[r++]}while(void 0!==s);else if(void 0!==a.toArray)do{a=s[i],void 0!==a&&(e.push(s.time),a.toArray(n,n.length)),s=t[r++]}while(void 0!==s);else do{a=s[i],void 0!==a&&(e.push(s.time),n.push(a)),s=t[r++]}while(void 0!==s)}const Ed={convertArray:Md,isTypedArray:Sd,getKeyframeOrder:bd,sortedArray:Td,flattenJSON:wd,subclip:function(t,e,n,i,r=30){const s=t.clone();s.name=e;const a=[];for(let t=0;t=i)){l.push(e.times[t]);for(let n=0;ns.tracks[t].times[0]&&(o=s.tracks[t].times[0]);for(let t=0;t=i.times[u]){const t=u*l+o,e=t+l-o;d=i.values.slice(t,e)}else{const t=i.createInterpolant(),e=o,n=l-o;t.evaluate(s),d=t.resultBuffer.slice(e,n)}if("quaternion"===r){(new Ri).fromArray(d).normalize().conjugate().toArray(d)}const p=a.times.length;for(let t=0;t=r)break t;{const a=e[1];t=r)break e}s=n,n=0}}for(;n>>1;te;)--s;if(++s,0!==r||s!==i){r>=s&&(s=Math.max(s,1),r=s-1);const t=this.getValueSize();this.times=n.slice(r,s),this.values=this.values.slice(r*t,s*t)}return this}validate(){let t=!0;const e=this.getValueSize();e-Math.floor(e)!=0&&(console.error("THREE.KeyframeTrack: Invalid value size in track.",this),t=!1);const n=this.times,i=this.values,r=n.length;0===r&&(console.error("THREE.KeyframeTrack: Track is empty.",this),t=!1);let s=null;for(let e=0;e!==r;e++){const i=n[e];if("number"==typeof i&&isNaN(i)){console.error("THREE.KeyframeTrack: Time is not a valid number.",this,e,i),t=!1;break}if(null!==s&&s>i){console.error("THREE.KeyframeTrack: Out of order keys.",this,e,i,s),t=!1;break}s=i}if(void 0!==i&&Sd(i))for(let e=0,n=i.length;e!==n;++e){const n=i[e];if(isNaN(n)){console.error("THREE.KeyframeTrack: Value is not a valid number.",this,e,n),t=!1;break}}return t}optimize(){const t=this.times.slice(),e=this.values.slice(),n=this.getValueSize(),i=this.getInterpolation()===De,r=t.length-1;let s=1;for(let a=1;a0){t[s]=t[r];for(let t=r*n,i=s*n,a=0;a!==n;++a)e[i+a]=e[t+a];++s}return s!==t.length?(this.times=t.slice(0,s),this.values=e.slice(0,s*n)):(this.times=t,this.values=e),this}clone(){const t=this.times.slice(),e=this.values.slice(),n=new(0,this.constructor)(this.name,t,e);return n.createInterpolant=this.createInterpolant,n}}Pd.prototype.TimeBufferType=Float32Array,Pd.prototype.ValueBufferType=Float32Array,Pd.prototype.DefaultInterpolation=Ue;class Ld extends Pd{constructor(t,e,n){super(t,e,n)}}Ld.prototype.ValueTypeName="bool",Ld.prototype.ValueBufferType=Array,Ld.prototype.DefaultInterpolation=Le,Ld.prototype.InterpolantFactoryMethodLinear=void 0,Ld.prototype.InterpolantFactoryMethodSmooth=void 0;class Ud extends Pd{}Ud.prototype.ValueTypeName="color";class Dd extends Pd{}Dd.prototype.ValueTypeName="number";class Nd extends Ad{constructor(t,e,n,i){super(t,e,n,i)}interpolate_(t,e,n,i){const r=this.resultBuffer,s=this.sampleValues,a=this.valueSize,o=(n-e)/(i-e);let l=t*a;for(let t=l+a;l!==t;l+=4)Ri.slerpFlat(r,0,s,l-a,s,l,o);return r}}class Od extends Pd{InterpolantFactoryMethodLinear(t){return new Nd(this.times,this.values,this.getValueSize(),t)}}Od.prototype.ValueTypeName="quaternion",Od.prototype.InterpolantFactoryMethodSmooth=void 0;class Fd extends Pd{constructor(t,e,n){super(t,e,n)}}Fd.prototype.ValueTypeName="string",Fd.prototype.ValueBufferType=Array,Fd.prototype.DefaultInterpolation=Le,Fd.prototype.InterpolantFactoryMethodLinear=void 0,Fd.prototype.InterpolantFactoryMethodSmooth=void 0;class Bd extends Pd{}Bd.prototype.ValueTypeName="vector";class zd{constructor(t="",e=-1,n=[],i=2500){this.name=t,this.tracks=n,this.duration=e,this.blendMode=i,this.uuid=Gn(),this.duration<0&&this.resetDuration()}static parse(t){const e=[],n=t.tracks,i=1/(t.fps||1);for(let t=0,r=n.length;t!==r;++t)e.push(kd(n[t]).scale(i));const r=new this(t.name,t.duration,e,t.blendMode);return r.uuid=t.uuid,r}static toJSON(t){const e=[],n=t.tracks,i={name:t.name,duration:t.duration,tracks:e,uuid:t.uuid,blendMode:t.blendMode};for(let t=0,i=n.length;t!==i;++t)e.push(Pd.toJSON(n[t]));return i}static CreateFromMorphTargetSequence(t,e,n,i){const r=e.length,s=[];for(let t=0;t1){const t=s[1];let e=i[t];e||(i[t]=e=[]),e.push(n)}}const s=[];for(const t in i)s.push(this.CreateFromMorphTargetSequence(t,i[t],e,n));return s}static parseAnimation(t,e){if(!t)return console.error("THREE.AnimationClip: No animation in JSONLoader data."),null;const n=function(t,e,n,i,r){if(0!==n.length){const s=[],a=[];wd(n,s,a,i),0!==s.length&&r.push(new t(e,s,a))}},i=[],r=t.name||"default",s=t.fps||30,a=t.blendMode;let o=t.length||-1;const l=t.hierarchy||[];for(let t=0;t{e&&e(r),this.manager.itemEnd(t)}),0),r;if(void 0!==Xd[t])return void Xd[t].push({onLoad:e,onProgress:n,onError:i});Xd[t]=[],Xd[t].push({onLoad:e,onProgress:n,onError:i});const s=new Request(t,{headers:new Headers(this.requestHeader),credentials:this.withCredentials?"include":"same-origin"}),a=this.mimeType,o=this.responseType;fetch(s).then((e=>{if(200===e.status||0===e.status){if(0===e.status&&console.warn("THREE.FileLoader: HTTP Status 0 received."),"undefined"==typeof ReadableStream||void 0===e.body||void 0===e.body.getReader)return e;const n=Xd[t],i=e.body.getReader(),r=e.headers.get("X-File-Size")||e.headers.get("Content-Length"),s=r?parseInt(r):0,a=0!==s;let o=0;const l=new ReadableStream({start(t){!function e(){i.read().then((({done:i,value:r})=>{if(i)t.close();else{o+=r.byteLength;const i=new ProgressEvent("progress",{lengthComputable:a,loaded:o,total:s});for(let t=0,e=n.length;t{t.error(e)}))}()}});return new Response(l)}throw new jd(`fetch for "${e.url}" responded with ${e.status}: ${e.statusText}`,e)})).then((t=>{switch(o){case"arraybuffer":return t.arrayBuffer();case"blob":return t.blob();case"document":return t.text().then((t=>(new DOMParser).parseFromString(t,a)));case"json":return t.json();default:if(void 0===a)return t.text();{const e=/charset="?([^;"\s]*)"?/i.exec(a),n=e&&e[1]?e[1].toLowerCase():void 0,i=new TextDecoder(n);return t.arrayBuffer().then((t=>i.decode(t)))}}})).then((e=>{Vd.add(t,e);const n=Xd[t];delete Xd[t];for(let t=0,i=n.length;t{const n=Xd[t];if(void 0===n)throw this.manager.itemError(t),e;delete Xd[t];for(let t=0,i=n.length;t{this.manager.itemEnd(t)})),this.manager.itemStart(t)}setResponseType(t){return this.responseType=t,this}setMimeType(t){return this.mimeType=t,this}}class Yd extends Wd{constructor(t){super(t)}load(t,e,n,i){const r=this,s=new qd(this.manager);s.setPath(this.path),s.setRequestHeader(this.requestHeader),s.setWithCredentials(this.withCredentials),s.load(t,(function(n){try{e(r.parse(JSON.parse(n)))}catch(e){i?i(e):console.error(e),r.manager.itemError(t)}}),n,i)}parse(t){const e=[];for(let n=0;n0:i.vertexColors=t.vertexColors),void 0!==t.uniforms)for(const e in t.uniforms){const r=t.uniforms[e];switch(i.uniforms[e]={},r.type){case"t":i.uniforms[e].value=n(r.value);break;case"c":i.uniforms[e].value=(new Kr).setHex(r.value);break;case"v2":i.uniforms[e].value=(new Jn).fromArray(r.value);break;case"v3":i.uniforms[e].value=(new Ci).fromArray(r.value);break;case"v4":i.uniforms[e].value=(new Mi).fromArray(r.value);break;case"m3":i.uniforms[e].value=(new Kn).fromArray(r.value);break;case"m4":i.uniforms[e].value=(new sr).fromArray(r.value);break;default:i.uniforms[e].value=r.value}}if(void 0!==t.defines&&(i.defines=t.defines),void 0!==t.vertexShader&&(i.vertexShader=t.vertexShader),void 0!==t.fragmentShader&&(i.fragmentShader=t.fragmentShader),void 0!==t.glslVersion&&(i.glslVersion=t.glslVersion),void 0!==t.extensions)for(const e in t.extensions)i.extensions[e]=t.extensions[e];if(void 0!==t.lights&&(i.lights=t.lights),void 0!==t.clipping&&(i.clipping=t.clipping),void 0!==t.size&&(i.size=t.size),void 0!==t.sizeAttenuation&&(i.sizeAttenuation=t.sizeAttenuation),void 0!==t.map&&(i.map=n(t.map)),void 0!==t.matcap&&(i.matcap=n(t.matcap)),void 0!==t.alphaMap&&(i.alphaMap=n(t.alphaMap)),void 0!==t.bumpMap&&(i.bumpMap=n(t.bumpMap)),void 0!==t.bumpScale&&(i.bumpScale=t.bumpScale),void 0!==t.normalMap&&(i.normalMap=n(t.normalMap)),void 0!==t.normalMapType&&(i.normalMapType=t.normalMapType),void 0!==t.normalScale){let e=t.normalScale;!1===Array.isArray(e)&&(e=[e,e]),i.normalScale=(new Jn).fromArray(e)}return void 0!==t.displacementMap&&(i.displacementMap=n(t.displacementMap)),void 0!==t.displacementScale&&(i.displacementScale=t.displacementScale),void 0!==t.displacementBias&&(i.displacementBias=t.displacementBias),void 0!==t.roughnessMap&&(i.roughnessMap=n(t.roughnessMap)),void 0!==t.metalnessMap&&(i.metalnessMap=n(t.metalnessMap)),void 0!==t.emissiveMap&&(i.emissiveMap=n(t.emissiveMap)),void 0!==t.emissiveIntensity&&(i.emissiveIntensity=t.emissiveIntensity),void 0!==t.specularMap&&(i.specularMap=n(t.specularMap)),void 0!==t.specularIntensityMap&&(i.specularIntensityMap=n(t.specularIntensityMap)),void 0!==t.specularColorMap&&(i.specularColorMap=n(t.specularColorMap)),void 0!==t.envMap&&(i.envMap=n(t.envMap)),void 0!==t.envMapRotation&&i.envMapRotation.fromArray(t.envMapRotation),void 0!==t.envMapIntensity&&(i.envMapIntensity=t.envMapIntensity),void 0!==t.reflectivity&&(i.reflectivity=t.reflectivity),void 0!==t.refractionRatio&&(i.refractionRatio=t.refractionRatio),void 0!==t.lightMap&&(i.lightMap=n(t.lightMap)),void 0!==t.lightMapIntensity&&(i.lightMapIntensity=t.lightMapIntensity),void 0!==t.aoMap&&(i.aoMap=n(t.aoMap)),void 0!==t.aoMapIntensity&&(i.aoMapIntensity=t.aoMapIntensity),void 0!==t.gradientMap&&(i.gradientMap=n(t.gradientMap)),void 0!==t.clearcoatMap&&(i.clearcoatMap=n(t.clearcoatMap)),void 0!==t.clearcoatRoughnessMap&&(i.clearcoatRoughnessMap=n(t.clearcoatRoughnessMap)),void 0!==t.clearcoatNormalMap&&(i.clearcoatNormalMap=n(t.clearcoatNormalMap)),void 0!==t.clearcoatNormalScale&&(i.clearcoatNormalScale=(new Jn).fromArray(t.clearcoatNormalScale)),void 0!==t.iridescenceMap&&(i.iridescenceMap=n(t.iridescenceMap)),void 0!==t.iridescenceThicknessMap&&(i.iridescenceThicknessMap=n(t.iridescenceThicknessMap)),void 0!==t.transmissionMap&&(i.transmissionMap=n(t.transmissionMap)),void 0!==t.thicknessMap&&(i.thicknessMap=n(t.thicknessMap)),void 0!==t.anisotropyMap&&(i.anisotropyMap=n(t.anisotropyMap)),void 0!==t.sheenColorMap&&(i.sheenColorMap=n(t.sheenColorMap)),void 0!==t.sheenRoughnessMap&&(i.sheenRoughnessMap=n(t.sheenRoughnessMap)),i}setTextures(t){return this.textures=t,this}createMaterialFromType(t){return xp.createMaterialFromType(t)}static createMaterialFromType(t){return new{ShadowMaterial:ud,SpriteMaterial:lc,RawShaderMaterial:dd,ShaderMaterial:Xs,PointsMaterial:Ch,MeshPhysicalMaterial:md,MeshStandardMaterial:pd,MeshPhongMaterial:fd,MeshToonMaterial:gd,MeshNormalMaterial:vd,MeshLambertMaterial:_d,MeshDepthMaterial:Ol,MeshDistanceMaterial:Fl,MeshBasicMaterial:es,MeshMatcapMaterial:xd,LineDashedMaterial:yd,LineBasicMaterial:fh,Material:ts}[t]}}class yp{static decodeText(t){if(console.warn("THREE.LoaderUtils: decodeText() has been deprecated with r165 and will be removed with r175. Use TextDecoder instead."),"undefined"!=typeof TextDecoder)return(new TextDecoder).decode(t);let e="";for(let n=0,i=t.length;n0){const n=new Hd(e);r=new Jd(n),r.setCrossOrigin(this.crossOrigin);for(let e=0,n=t.length;e0){i=new Jd(this.manager),i.setCrossOrigin(this.crossOrigin);for(let e=0,i=t.length;e{const e=new Li;e.min.fromArray(t.boxMin),e.max.fromArray(t.boxMax);const n=new Ji;return n.radius=t.sphereRadius,n.center.fromArray(t.sphereCenter),{boxInitialized:t.boxInitialized,box:e,sphereInitialized:t.sphereInitialized,sphere:n}})),s._maxInstanceCount=t.maxInstanceCount,s._maxVertexCount=t.maxVertexCount,s._maxIndexCount=t.maxIndexCount,s._geometryInitialized=t.geometryInitialized,s._geometryCount=t.geometryCount,s._matricesTexture=h(t.matricesTexture.uuid),void 0!==t.colorsTexture&&(s._colorsTexture=h(t.colorsTexture.uuid));break;case"LOD":s=new Ec;break;case"Line":s=new bh(l(t.geometry),c(t.material));break;case"LineLoop":s=new Rh(l(t.geometry),c(t.material));break;case"LineSegments":s=new Ah(l(t.geometry),c(t.material));break;case"PointCloud":case"Points":s=new Dh(l(t.geometry),c(t.material));break;case"Sprite":s=new Sc(c(t.material));break;case"Group":s=new jl;break;case"Bone":s=new Fc;break;default:s=new Lr}if(s.uuid=t.uuid,void 0!==t.name&&(s.name=t.name),void 0!==t.matrix?(s.matrix.fromArray(t.matrix),void 0!==t.matrixAutoUpdate&&(s.matrixAutoUpdate=t.matrixAutoUpdate),s.matrixAutoUpdate&&s.matrix.decompose(s.position,s.quaternion,s.scale)):(void 0!==t.position&&s.position.fromArray(t.position),void 0!==t.rotation&&s.rotation.fromArray(t.rotation),void 0!==t.quaternion&&s.quaternion.fromArray(t.quaternion),void 0!==t.scale&&s.scale.fromArray(t.scale)),void 0!==t.up&&s.up.fromArray(t.up),void 0!==t.castShadow&&(s.castShadow=t.castShadow),void 0!==t.receiveShadow&&(s.receiveShadow=t.receiveShadow),t.shadow&&(void 0!==t.shadow.intensity&&(s.shadow.intensity=t.shadow.intensity),void 0!==t.shadow.bias&&(s.shadow.bias=t.shadow.bias),void 0!==t.shadow.normalBias&&(s.shadow.normalBias=t.shadow.normalBias),void 0!==t.shadow.radius&&(s.shadow.radius=t.shadow.radius),void 0!==t.shadow.mapSize&&s.shadow.mapSize.fromArray(t.shadow.mapSize),void 0!==t.shadow.camera&&(s.shadow.camera=this.parseObject(t.shadow.camera))),void 0!==t.visible&&(s.visible=t.visible),void 0!==t.frustumCulled&&(s.frustumCulled=t.frustumCulled),void 0!==t.renderOrder&&(s.renderOrder=t.renderOrder),void 0!==t.userData&&(s.userData=t.userData),void 0!==t.layers&&(s.layers.mask=t.layers),void 0!==t.children){const a=t.children;for(let t=0;t{e&&e(n),r.manager.itemEnd(t)})).catch((t=>{i&&i(t)})):(setTimeout((function(){e&&e(s),r.manager.itemEnd(t)}),0),s);const a={};a.credentials="anonymous"===this.crossOrigin?"same-origin":"include",a.headers=this.requestHeader;const o=fetch(t,a).then((function(t){return t.blob()})).then((function(t){return createImageBitmap(t,Object.assign(r.options,{colorSpaceConversion:"none"}))})).then((function(n){return Vd.add(t,n),e&&e(n),r.manager.itemEnd(t),n})).catch((function(e){i&&i(e),Vd.remove(t),r.manager.itemError(t),r.manager.itemEnd(t)}));Vd.add(t,o),r.manager.itemStart(t)}}let Rp;class Cp{static getContext(){return void 0===Rp&&(Rp=new(window.AudioContext||window.webkitAudioContext)),Rp}static setContext(t){Rp=t}}class Ip extends Wd{constructor(t){super(t)}load(t,e,n,i){const r=this,s=new qd(this.manager);function a(e){i?i(e):console.error(e),r.manager.itemError(t)}s.setResponseType("arraybuffer"),s.setPath(this.path),s.setRequestHeader(this.requestHeader),s.setWithCredentials(this.withCredentials),s.load(t,(function(t){try{const n=t.slice(0);Cp.getContext().decodeAudioData(n,(function(t){e(t)})).catch(a)}catch(t){a(t)}}),n,i)}}const Pp=new sr,Lp=new sr,Up=new sr;class Dp{constructor(){this.type="StereoCamera",this.aspect=1,this.eyeSep=.064,this.cameraL=new Js,this.cameraL.layers.enable(1),this.cameraL.matrixAutoUpdate=!1,this.cameraR=new Js,this.cameraR.layers.enable(2),this.cameraR.matrixAutoUpdate=!1,this._cache={focus:null,fov:null,aspect:null,near:null,far:null,zoom:null,eyeSep:null}}update(t){const e=this._cache;if(e.focus!==t.focus||e.fov!==t.fov||e.aspect!==t.aspect*this.aspect||e.near!==t.near||e.far!==t.far||e.zoom!==t.zoom||e.eyeSep!==this.eyeSep){e.focus=t.focus,e.fov=t.fov,e.aspect=t.aspect*this.aspect,e.near=t.near,e.far=t.far,e.zoom=t.zoom,e.eyeSep=this.eyeSep,Up.copy(t.projectionMatrix);const n=e.eyeSep/2,i=n*e.near/e.focus,r=e.near*Math.tan(Vn*e.fov*.5)/e.zoom;let s,a;Lp.elements[12]=-n,Pp.elements[12]=n,s=-r*e.aspect+i,a=r*e.aspect+i,Up.elements[0]=2*e.near/(a-s),Up.elements[8]=(a+s)/(a-s),this.cameraL.projectionMatrix.copy(Up),s=-r*e.aspect-i,a=r*e.aspect-i,Up.elements[0]=2*e.near/(a-s),Up.elements[8]=(a+s)/(a-s),this.cameraR.projectionMatrix.copy(Up)}this.cameraL.matrixWorld.copy(t.matrixWorld).multiply(Lp),this.cameraR.matrixWorld.copy(t.matrixWorld).multiply(Pp)}}class Np{constructor(t=!0){this.autoStart=t,this.startTime=0,this.oldTime=0,this.elapsedTime=0,this.running=!1}start(){this.startTime=Op(),this.oldTime=this.startTime,this.elapsedTime=0,this.running=!0}stop(){this.getElapsedTime(),this.running=!1,this.autoStart=!1}getElapsedTime(){return this.getDelta(),this.elapsedTime}getDelta(){let t=0;if(this.autoStart&&!this.running)return this.start(),0;if(this.running){const e=Op();t=(e-this.oldTime)/1e3,this.oldTime=e,this.elapsedTime+=t}return t}}function Op(){return performance.now()}const Fp=new Ci,Bp=new Ri,zp=new Ci,kp=new Ci;class Vp extends Lr{constructor(){super(),this.type="AudioListener",this.context=Cp.getContext(),this.gain=this.context.createGain(),this.gain.connect(this.context.destination),this.filter=null,this.timeDelta=0,this._clock=new Np}getInput(){return this.gain}removeFilter(){return null!==this.filter&&(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination),this.gain.connect(this.context.destination),this.filter=null),this}getFilter(){return this.filter}setFilter(t){return null!==this.filter?(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination)):this.gain.disconnect(this.context.destination),this.filter=t,this.gain.connect(this.filter),this.filter.connect(this.context.destination),this}getMasterVolume(){return this.gain.gain.value}setMasterVolume(t){return this.gain.gain.setTargetAtTime(t,this.context.currentTime,.01),this}updateMatrixWorld(t){super.updateMatrixWorld(t);const e=this.context.listener,n=this.up;if(this.timeDelta=this._clock.getDelta(),this.matrixWorld.decompose(Fp,Bp,zp),kp.set(0,0,-1).applyQuaternion(Bp),e.positionX){const t=this.context.currentTime+this.timeDelta;e.positionX.linearRampToValueAtTime(Fp.x,t),e.positionY.linearRampToValueAtTime(Fp.y,t),e.positionZ.linearRampToValueAtTime(Fp.z,t),e.forwardX.linearRampToValueAtTime(kp.x,t),e.forwardY.linearRampToValueAtTime(kp.y,t),e.forwardZ.linearRampToValueAtTime(kp.z,t),e.upX.linearRampToValueAtTime(n.x,t),e.upY.linearRampToValueAtTime(n.y,t),e.upZ.linearRampToValueAtTime(n.z,t)}else e.setPosition(Fp.x,Fp.y,Fp.z),e.setOrientation(kp.x,kp.y,kp.z,n.x,n.y,n.z)}}class Hp extends Lr{constructor(t){super(),this.type="Audio",this.listener=t,this.context=t.context,this.gain=this.context.createGain(),this.gain.connect(t.getInput()),this.autoplay=!1,this.buffer=null,this.detune=0,this.loop=!1,this.loopStart=0,this.loopEnd=0,this.offset=0,this.duration=void 0,this.playbackRate=1,this.isPlaying=!1,this.hasPlaybackControl=!0,this.source=null,this.sourceType="empty",this._startedAt=0,this._progress=0,this._connected=!1,this.filters=[]}getOutput(){return this.gain}setNodeSource(t){return this.hasPlaybackControl=!1,this.sourceType="audioNode",this.source=t,this.connect(),this}setMediaElementSource(t){return this.hasPlaybackControl=!1,this.sourceType="mediaNode",this.source=this.context.createMediaElementSource(t),this.connect(),this}setMediaStreamSource(t){return this.hasPlaybackControl=!1,this.sourceType="mediaStreamNode",this.source=this.context.createMediaStreamSource(t),this.connect(),this}setBuffer(t){return this.buffer=t,this.sourceType="buffer",this.autoplay&&this.play(),this}play(t=0){if(!0===this.isPlaying)return void console.warn("THREE.Audio: Audio is already playing.");if(!1===this.hasPlaybackControl)return void console.warn("THREE.Audio: this Audio has no playback control.");this._startedAt=this.context.currentTime+t;const e=this.context.createBufferSource();return e.buffer=this.buffer,e.loop=this.loop,e.loopStart=this.loopStart,e.loopEnd=this.loopEnd,e.onended=this.onEnded.bind(this),e.start(this._startedAt,this._progress+this.offset,this.duration),this.isPlaying=!0,this.source=e,this.setDetune(this.detune),this.setPlaybackRate(this.playbackRate),this.connect()}pause(){if(!1!==this.hasPlaybackControl)return!0===this.isPlaying&&(this._progress+=Math.max(this.context.currentTime-this._startedAt,0)*this.playbackRate,!0===this.loop&&(this._progress=this._progress%(this.duration||this.buffer.duration)),this.source.stop(),this.source.onended=null,this.isPlaying=!1),this;console.warn("THREE.Audio: this Audio has no playback control.")}stop(t=0){if(!1!==this.hasPlaybackControl)return this._progress=0,null!==this.source&&(this.source.stop(this.context.currentTime+t),this.source.onended=null),this.isPlaying=!1,this;console.warn("THREE.Audio: this Audio has no playback control.")}connect(){if(this.filters.length>0){this.source.connect(this.filters[0]);for(let t=1,e=this.filters.length;t0){this.source.disconnect(this.filters[0]);for(let t=1,e=this.filters.length;t0&&this._mixBufferRegionAdditive(n,i,this._addIndex*e,1,e);for(let t=e,r=e+e;t!==r;++t)if(n[t]!==n[t+e]){a.setValue(n,i);break}}saveOriginalState(){const t=this.binding,e=this.buffer,n=this.valueSize,i=n*this._origIndex;t.getValue(e,i);for(let t=n,r=i;t!==r;++t)e[t]=e[i+t%n];this._setIdentity(),this.cumulativeWeight=0,this.cumulativeWeightAdditive=0}restoreOriginalState(){const t=3*this.valueSize;this.binding.setValue(this.buffer,t)}_setAdditiveIdentityNumeric(){const t=this._addIndex*this.valueSize,e=t+this.valueSize;for(let n=t;n=.5)for(let i=0;i!==r;++i)t[e+i]=t[n+i]}_slerp(t,e,n,i){Ri.slerpFlat(t,e,t,e,t,n,i)}_slerpAdditive(t,e,n,i,r){const s=this._workIndex*r;Ri.multiplyQuaternionsFlat(t,s,t,e,t,n),Ri.slerpFlat(t,e,t,e,t,s,i)}_lerp(t,e,n,i,r){const s=1-i;for(let a=0;a!==r;++a){const r=e+a;t[r]=t[r]*s+t[n+a]*i}}_lerpAdditive(t,e,n,i,r){for(let s=0;s!==r;++s){const r=e+s;t[r]=t[r]+t[n+s]*i}}}const Jp="\\[\\]\\.:\\/",Kp=new RegExp("["+Jp+"]","g"),$p="[^"+Jp+"]",Qp="[^"+Jp.replace("\\.","")+"]",tm=new RegExp("^"+/((?:WC+[\/:])*)/.source.replace("WC",$p)+/(WCOD+)?/.source.replace("WCOD",Qp)+/(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace("WC",$p)+/\.(WC+)(?:\[(.+)\])?/.source.replace("WC",$p)+"$"),em=["material","materials","bones","map"];class nm{constructor(t,e,n){this.path=e,this.parsedPath=n||nm.parseTrackName(e),this.node=nm.findNode(t,this.parsedPath.nodeName),this.rootNode=t,this.getValue=this._getValue_unbound,this.setValue=this._setValue_unbound}static create(t,e,n){return t&&t.isAnimationObjectGroup?new nm.Composite(t,e,n):new nm(t,e,n)}static sanitizeNodeName(t){return t.replace(/\s/g,"_").replace(Kp,"")}static parseTrackName(t){const e=tm.exec(t);if(null===e)throw new Error("PropertyBinding: Cannot parse trackName: "+t);const n={nodeName:e[2],objectName:e[3],objectIndex:e[4],propertyName:e[5],propertyIndex:e[6]},i=n.nodeName&&n.nodeName.lastIndexOf(".");if(void 0!==i&&-1!==i){const t=n.nodeName.substring(i+1);-1!==em.indexOf(t)&&(n.nodeName=n.nodeName.substring(0,i),n.objectName=t)}if(null===n.propertyName||0===n.propertyName.length)throw new Error("PropertyBinding: can not parse propertyName from trackName: "+t);return n}static findNode(t,e){if(void 0===e||""===e||"."===e||-1===e||e===t.name||e===t.uuid)return t;if(t.skeleton){const n=t.skeleton.getBoneByName(e);if(void 0!==n)return n}if(t.children){const n=function(t){for(let i=0;i=r){const s=r++,c=t[s];e[c.uuid]=l,t[l]=c,e[o]=s,t[s]=a;for(let t=0,e=i;t!==e;++t){const e=n[t],i=e[s],r=e[l];e[l]=i,e[s]=r}}}this.nCachedObjects_=r}uncache(){const t=this._objects,e=this._indicesByUUID,n=this._bindings,i=n.length;let r=this.nCachedObjects_,s=t.length;for(let a=0,o=arguments.length;a!==o;++a){const o=arguments[a].uuid,l=e[o];if(void 0!==l)if(delete e[o],l0&&(e[a.uuid]=l),t[l]=a,t.pop();for(let t=0,e=i;t!==e;++t){const e=n[t];e[l]=e[r],e.pop()}}}this.nCachedObjects_=r}subscribe_(t,e){const n=this._bindingsIndicesByPath;let i=n[t];const r=this._bindings;if(void 0!==i)return r[i];const s=this._paths,a=this._parsedPaths,o=this._objects,l=o.length,c=this.nCachedObjects_,h=new Array(l);i=r.length,n[t]=i,s.push(t),a.push(e),r.push(h);for(let n=c,i=o.length;n!==i;++n){const i=o[n];h[n]=new nm(i,t,e)}return h}unsubscribe_(t){const e=this._bindingsIndicesByPath,n=e[t];if(void 0!==n){const i=this._paths,r=this._parsedPaths,s=this._bindings,a=s.length-1,o=s[a];e[t[a]]=n,s[n]=o,s.pop(),r[n]=r[a],r.pop(),i[n]=i[a],i.pop()}}}class rm{constructor(t,e,n=null,i=e.blendMode){this._mixer=t,this._clip=e,this._localRoot=n,this.blendMode=i;const r=e.tracks,s=r.length,a=new Array(s),o={endingStart:Ne,endingEnd:Ne};for(let t=0;t!==s;++t){const e=r[t].createInterpolant(null);a[t]=e,e.settings=o}this._interpolantSettings=o,this._interpolants=a,this._propertyBindings=new Array(s),this._cacheIndex=null,this._byClipCacheIndex=null,this._timeScaleInterpolant=null,this._weightInterpolant=null,this.loop=2201,this._loopCount=-1,this._startTime=null,this.time=0,this.timeScale=1,this._effectiveTimeScale=1,this.weight=1,this._effectiveWeight=1,this.repetitions=1/0,this.paused=!1,this.enabled=!0,this.clampWhenFinished=!1,this.zeroSlopeAtStart=!0,this.zeroSlopeAtEnd=!0}play(){return this._mixer._activateAction(this),this}stop(){return this._mixer._deactivateAction(this),this.reset()}reset(){return this.paused=!1,this.enabled=!0,this.time=0,this._loopCount=-1,this._startTime=null,this.stopFading().stopWarping()}isRunning(){return this.enabled&&!this.paused&&0!==this.timeScale&&null===this._startTime&&this._mixer._isActiveAction(this)}isScheduled(){return this._mixer._isActiveAction(this)}startAt(t){return this._startTime=t,this}setLoop(t,e){return this.loop=t,this.repetitions=e,this}setEffectiveWeight(t){return this.weight=t,this._effectiveWeight=this.enabled?t:0,this.stopFading()}getEffectiveWeight(){return this._effectiveWeight}fadeIn(t){return this._scheduleFading(t,0,1)}fadeOut(t){return this._scheduleFading(t,1,0)}crossFadeFrom(t,e,n){if(t.fadeOut(e),this.fadeIn(e),n){const n=this._clip.duration,i=t._clip.duration,r=i/n,s=n/i;t.warp(1,r,e),this.warp(s,1,e)}return this}crossFadeTo(t,e,n){return t.crossFadeFrom(this,e,n)}stopFading(){const t=this._weightInterpolant;return null!==t&&(this._weightInterpolant=null,this._mixer._takeBackControlInterpolant(t)),this}setEffectiveTimeScale(t){return this.timeScale=t,this._effectiveTimeScale=this.paused?0:t,this.stopWarping()}getEffectiveTimeScale(){return this._effectiveTimeScale}setDuration(t){return this.timeScale=this._clip.duration/t,this.stopWarping()}syncWith(t){return this.time=t.time,this.timeScale=t.timeScale,this.stopWarping()}halt(t){return this.warp(this._effectiveTimeScale,0,t)}warp(t,e,n){const i=this._mixer,r=i.time,s=this.timeScale;let a=this._timeScaleInterpolant;null===a&&(a=i._lendControlInterpolant(),this._timeScaleInterpolant=a);const o=a.parameterPositions,l=a.sampleValues;return o[0]=r,o[1]=r+n,l[0]=t/s,l[1]=e/s,this}stopWarping(){const t=this._timeScaleInterpolant;return null!==t&&(this._timeScaleInterpolant=null,this._mixer._takeBackControlInterpolant(t)),this}getMixer(){return this._mixer}getClip(){return this._clip}getRoot(){return this._localRoot||this._mixer._root}_update(t,e,n,i){if(!this.enabled)return void this._updateWeight(t);const r=this._startTime;if(null!==r){const i=(t-r)*n;i<0||0===n?e=0:(this._startTime=null,e=n*i)}e*=this._updateTimeScale(t);const s=this._updateTime(e),a=this._updateWeight(t);if(a>0){const t=this._interpolants,e=this._propertyBindings;if(this.blendMode===ze)for(let n=0,i=t.length;n!==i;++n)t[n].evaluate(s),e[n].accumulateAdditive(a);else for(let n=0,r=t.length;n!==r;++n)t[n].evaluate(s),e[n].accumulate(i,a)}}_updateWeight(t){let e=0;if(this.enabled){e=this.weight;const n=this._weightInterpolant;if(null!==n){const i=n.evaluate(t)[0];e*=i,t>n.parameterPositions[1]&&(this.stopFading(),0===i&&(this.enabled=!1))}}return this._effectiveWeight=e,e}_updateTimeScale(t){let e=0;if(!this.paused){e=this.timeScale;const n=this._timeScaleInterpolant;if(null!==n){e*=n.evaluate(t)[0],t>n.parameterPositions[1]&&(this.stopWarping(),0===e?this.paused=!0:this.timeScale=e)}}return this._effectiveTimeScale=e,e}_updateTime(t){const e=this._clip.duration,n=this.loop;let i=this.time+t,r=this._loopCount;const s=2202===n;if(0===t)return-1===r||!s||1&~r?i:e-i;if(2200===n){-1===r&&(this._loopCount=0,this._setEndings(!0,!0,!1));t:{if(i>=e)i=e;else{if(!(i<0)){this.time=i;break t}i=0}this.clampWhenFinished?this.paused=!0:this.enabled=!1,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:t<0?-1:1})}}else{if(-1===r&&(t>=0?(r=0,this._setEndings(!0,0===this.repetitions,s)):this._setEndings(0===this.repetitions,!0,s)),i>=e||i<0){const n=Math.floor(i/e);i-=e*n,r+=Math.abs(n);const a=this.repetitions-r;if(a<=0)this.clampWhenFinished?this.paused=!0:this.enabled=!1,i=t>0?e:0,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:t>0?1:-1});else{if(1===a){const e=t<0;this._setEndings(e,!e,s)}else this._setEndings(!1,!1,s);this._loopCount=r,this.time=i,this._mixer.dispatchEvent({type:"loop",action:this,loopDelta:n})}}else this.time=i;if(s&&!(1&~r))return e-i}return i}_setEndings(t,e,n){const i=this._interpolantSettings;n?(i.endingStart=Oe,i.endingEnd=Oe):(i.endingStart=t?this.zeroSlopeAtStart?Oe:Ne:Fe,i.endingEnd=e?this.zeroSlopeAtEnd?Oe:Ne:Fe)}_scheduleFading(t,e,n){const i=this._mixer,r=i.time;let s=this._weightInterpolant;null===s&&(s=i._lendControlInterpolant(),this._weightInterpolant=s);const a=s.parameterPositions,o=s.sampleValues;return a[0]=r,o[0]=e,a[1]=r+t,o[1]=n,this}}const sm=new Float32Array(1);class am extends Bn{constructor(t){super(),this._root=t,this._initMemoryManager(),this._accuIndex=0,this.time=0,this.timeScale=1}_bindAction(t,e){const n=t._localRoot||this._root,i=t._clip.tracks,r=i.length,s=t._propertyBindings,a=t._interpolants,o=n.uuid,l=this._bindingsByRootAndName;let c=l[o];void 0===c&&(c={},l[o]=c);for(let t=0;t!==r;++t){const r=i[t],l=r.name;let h=c[l];if(void 0!==h)++h.referenceCount,s[t]=h;else{if(h=s[t],void 0!==h){null===h._cacheIndex&&(++h.referenceCount,this._addInactiveBinding(h,o,l));continue}const i=e&&e._propertyBindings[t].binding.parsedPath;h=new Zp(nm.create(n,l,i),r.ValueTypeName,r.getValueSize()),++h.referenceCount,this._addInactiveBinding(h,o,l),s[t]=h}a[t].resultBuffer=h.buffer}}_activateAction(t){if(!this._isActiveAction(t)){if(null===t._cacheIndex){const e=(t._localRoot||this._root).uuid,n=t._clip.uuid,i=this._actionsByClip[n];this._bindAction(t,i&&i.knownActions[0]),this._addInactiveAction(t,n,e)}const e=t._propertyBindings;for(let t=0,n=e.length;t!==n;++t){const n=e[t];0==n.useCount++&&(this._lendBinding(n),n.saveOriginalState())}this._lendAction(t)}}_deactivateAction(t){if(this._isActiveAction(t)){const e=t._propertyBindings;for(let t=0,n=e.length;t!==n;++t){const n=e[t];0==--n.useCount&&(n.restoreOriginalState(),this._takeBackBinding(n))}this._takeBackAction(t)}}_initMemoryManager(){this._actions=[],this._nActiveActions=0,this._actionsByClip={},this._bindings=[],this._nActiveBindings=0,this._bindingsByRootAndName={},this._controlInterpolants=[],this._nActiveControlInterpolants=0;const t=this;this.stats={actions:{get total(){return t._actions.length},get inUse(){return t._nActiveActions}},bindings:{get total(){return t._bindings.length},get inUse(){return t._nActiveBindings}},controlInterpolants:{get total(){return t._controlInterpolants.length},get inUse(){return t._nActiveControlInterpolants}}}}_isActiveAction(t){const e=t._cacheIndex;return null!==e&&e=0;--e)t[e].stop();return this}update(t){t*=this.timeScale;const e=this._actions,n=this._nActiveActions,i=this.time+=t,r=Math.sign(t),s=this._accuIndex^=1;for(let a=0;a!==n;++a){e[a]._update(i,t,r,s)}const a=this._bindings,o=this._nActiveBindings;for(let t=0;t!==o;++t)a[t].apply(s);return this}setTime(t){this.time=0;for(let t=0;t=this.min.x&&t.x<=this.max.x&&t.y>=this.min.y&&t.y<=this.max.y}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y}getParameter(t,e){return e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y))}intersectsBox(t){return t.max.x>=this.min.x&&t.min.x<=this.max.x&&t.max.y>=this.min.y&&t.min.y<=this.max.y}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return this.clampPoint(t,xm).distanceTo(t)}intersect(t){return this.min.max(t.min),this.max.min(t.max),this.isEmpty()&&this.makeEmpty(),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}}const Mm=new Ci,Sm=new Ci;class bm{constructor(t=new Ci,e=new Ci){this.start=t,this.end=e}set(t,e){return this.start.copy(t),this.end.copy(e),this}copy(t){return this.start.copy(t.start),this.end.copy(t.end),this}getCenter(t){return t.addVectors(this.start,this.end).multiplyScalar(.5)}delta(t){return t.subVectors(this.end,this.start)}distanceSq(){return this.start.distanceToSquared(this.end)}distance(){return this.start.distanceTo(this.end)}at(t,e){return this.delta(e).multiplyScalar(t).add(this.start)}closestPointToPointParameter(t,e){Mm.subVectors(t,this.start),Sm.subVectors(this.end,this.start);const n=Sm.dot(Sm);let i=Sm.dot(Mm)/n;return e&&(i=Wn(i,0,1)),i}closestPointToPoint(t,e,n){const i=this.closestPointToPointParameter(t,e);return this.delta(n).multiplyScalar(i).add(this.start)}applyMatrix4(t){return this.start.applyMatrix4(t),this.end.applyMatrix4(t),this}equals(t){return t.start.equals(this.start)&&t.end.equals(this.end)}clone(){return(new this.constructor).copy(this)}}const Tm=new Ci;class wm extends Lr{constructor(t,e){super(),this.light=t,this.matrixAutoUpdate=!1,this.color=e,this.type="SpotLightHelper";const n=new Es,i=[0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,-1,0,1,0,0,0,0,1,1,0,0,0,0,-1,1];for(let t=0,e=1,n=32;t1)for(let n=0;n.99999)this.quaternion.set(0,0,0,1);else if(t.y<-.99999)this.quaternion.set(1,0,0,0);else{Jm.set(t.z,0,-t.x).normalize();const e=Math.acos(t.y);this.quaternion.setFromAxisAngle(Jm,e)}}setLength(t,e=.2*t,n=.2*e){this.line.scale.set(1,Math.max(1e-4,t-e),1),this.line.updateMatrix(),this.cone.scale.set(n,e,n),this.cone.position.y=t,this.cone.updateMatrix()}setColor(t){this.line.material.color.set(t),this.cone.material.color.set(t)}copy(t){return super.copy(t,!1),this.line.copy(t.line),this.cone.copy(t.cone),this}dispose(){this.line.geometry.dispose(),this.line.material.dispose(),this.cone.geometry.dispose(),this.cone.material.dispose()}}class tf extends Ah{constructor(t=1){const e=[0,0,0,t,0,0,0,0,0,0,t,0,0,0,0,0,0,t],n=new Es;n.setAttribute("position",new _s(e,3)),n.setAttribute("color",new _s([1,0,0,1,.6,0,0,1,0,.6,1,0,0,0,1,0,.6,1],3));super(n,new fh({vertexColors:!0,toneMapped:!1})),this.type="AxesHelper"}setColors(t,e,n){const i=new Kr,r=this.geometry.attributes.color.array;return i.set(t),i.toArray(r,0),i.toArray(r,3),i.set(e),i.toArray(r,6),i.toArray(r,9),i.set(n),i.toArray(r,12),i.toArray(r,15),this.geometry.attributes.color.needsUpdate=!0,this}dispose(){this.geometry.dispose(),this.material.dispose()}}class ef{constructor(){this.type="ShapePath",this.color=new Kr,this.subPaths=[],this.currentPath=null}moveTo(t,e){return this.currentPath=new cu,this.subPaths.push(this.currentPath),this.currentPath.moveTo(t,e),this}lineTo(t,e){return this.currentPath.lineTo(t,e),this}quadraticCurveTo(t,e,n,i){return this.currentPath.quadraticCurveTo(t,e,n,i),this}bezierCurveTo(t,e,n,i,r,s){return this.currentPath.bezierCurveTo(t,e,n,i,r,s),this}splineThru(t){return this.currentPath.splineThru(t),this}toShapes(t){function e(t,e){const n=e.length;let i=!1;for(let r=n-1,s=0;sNumber.EPSILON){if(l<0&&(n=e[s],o=-o,a=e[r],l=-l),t.ya.y)continue;if(t.y===n.y){if(t.x===n.x)return!0}else{const e=l*(t.x-n.x)-o*(t.y-n.y);if(0===e)return!0;if(e<0)continue;i=!i}}else{if(t.y!==n.y)continue;if(a.x<=t.x&&t.x<=n.x||n.x<=t.x&&t.x<=a.x)return!0}}return i}const n=Yu.isClockWise,i=this.subPaths;if(0===i.length)return[];let r,s,a;const o=[];if(1===i.length)return s=i[0],a=new Su,a.curves=s.curves,o.push(a),o;let l=!n(i[0].getPoints());l=t?!l:l;const c=[],h=[];let u,d,p=[],m=0;h[m]=void 0,p[m]=[];for(let e=0,a=i.length;e1){let t=!1,n=0;for(let t=0,e=h.length;t0&&!1===t&&(p=c)}for(let t=0,e=h.length;t Uint8Array.from(atob(s), (c) => c.charCodeAt(0)).buffer; +/** The mark's geometry as typed arrays. */ +export function decode() { + return { positions: new Float32Array(bytes(positions)), normals: normals ? new Float32Array(bytes(normals)) : null, indices: new Uint16Array(bytes(indices)) }; +} diff --git a/docs/_static/vortex-status-mark.js b/docs/_static/vortex-status-mark.js new file mode 100644 index 00000000000..e33a4dd27c2 --- /dev/null +++ b/docs/_static/vortex-status-mark.js @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +// The 404's mark, as vortex.dev draws it (the design system's AsciiMark): the Vortex mark turning +// in 3D, drawn as Geist Mono characters. The cell stays empty while the page loads. Once the page +// is idle, the renderer and three.js load and the live or reduced-motion mark mounts. Without +// WebGL, when three.js cannot load, or when mounting throws, the static symbol shows, and the +// host's data-mark-mode says why. three.js is a copy the docs serve, because their CSP allows +// scripts from this origin only. +const host = document.querySelector("[data-ascii-mark]"); + +async function mount() { + // A three.js module without WebGLRenderer makes mountAsciiMark return a static mark, so a file + // that fails to load leaves the symbol. + const [{ mountAsciiMark }, three] = await Promise.all([ + import("./vortex-mark/ascii-mark.js"), + import("./vortex-mark/three.module.min.js").catch(() => ({})), + ]); + // The DotField under the cell draws the dots, so the mark leaves its own. + const mark = await mountAsciiMark(host, { three, field: false }); + host.dataset.markMode = mark.mode; +} + +function run() { + mount().catch(() => { + host.dataset.markMode = "error"; + }); +} + +if (host) { + const idle = () => + typeof requestIdleCallback === "function" ? requestIdleCallback(run, { timeout: 2000 }) : setTimeout(run, 200); + if (document.readyState === "complete") idle(); + else addEventListener("load", idle, { once: true }); +} diff --git a/docs/_static/vortex-theme-bootstrap.js b/docs/_static/vortex-theme-bootstrap.js new file mode 100644 index 00000000000..238931e52ad --- /dev/null +++ b/docs/_static/vortex-theme-bootstrap.js @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +// The design system's THEME_BOOTSTRAP (components/lib/theme.js), as a file, because the docs' CSP +// allows only scripts from their own origin. Load it in , before the stylesheets. +(function(){var r=document.documentElement,c="dark",v=null,t=null,ok=!t;if(t){var p=location.pathname;for(var i=0;i. The choice itself also lands on , so the switch +// can show it pressed before its script runs. +// +// A site whose other pages are Dark only passes `themeable`, a list of path prefixes: "/blog" +// covers /blog and every page under it. Pages outside the list stay Dark whatever the choice. +// It is a list and not a function because the bootstrap carries it into an inline script as data. +const KEY = "vx-theme"; +const CHOICES = ["light", "dark", "system"]; +const PLAIN_PATH = /^\/[\w.~\/-]*$/; +const DEFAULT = "dark"; +const YEAR = 31536000; +const COOKIE = /(?:^|;\s*)vx-theme=([^;]*)/; + +// "/blog/" and "/blog" are one prefix. "/" stays "/". +const trim = (prefix) => (prefix.length > 1 ? prefix.replace(/\/+$/, "") : prefix); + +/** Whether `path` takes the reader's theme. With no list, every path does. */ +export function isThemeable(path, themeable) { + if (!themeable) return true; + return themeable.map(trim).some((p) => path === p || path.startsWith(p === "/" ? "/" : `${p}/`)); +} + +/** + * The no-flash snippet for a site where only `themeable` paths follow the choice (every path when + * it is left out). Paste it into an inline classic +{%- endblock %} +{%- block frame %} +
+
+ + +
+
+
+{%- endblock %} diff --git a/docs/_theme/vortex/layout.html b/docs/_theme/vortex/layout.html index ab8d7d7719d..183fa86b1e3 100644 --- a/docs/_theme/vortex/layout.html +++ b/docs/_theme/vortex/layout.html @@ -1,11 +1,16 @@ -{%- if not embedded and docstitle %} - {%- set titlesuffix = " — "|safe + docstitle|e %} -{%- else %} - {%- set titlesuffix = "" %} -{%- endif %} +{#- Titles are " | Vortex" on every surface (the design system's DECISIONS 36). The docs' + root page is "Docs". #} +{%- set page_title = _("Docs") if pagename == root_doc else title|striptags %} {%- set github = "https://github.com/vortex-data/vortex" %} - +{%- set site = "https://vortex.dev" %} +{#- The 404 is the one 404 the monorepo builds: explore.vortex.dev serves it too. So its links to the + docs are absolute, and its header marks no page current, as vortex.dev's 404 does. #} +{%- set is_404 = pagename == "404" %} +{%- set docs_home = "https://docs.vortex.dev/" if is_404 else pathto(root_doc) %} +{%- set docs_search = "https://docs.vortex.dev/search" if is_404 else pathto('search') %} +{%- set docs_current = "" if is_404 else "page" if pagename == root_doc else "true" %} + {{- metatags }} @@ -13,22 +18,27 @@ {%- endif %} {%- block htmltitle %} - {{ title|striptags|e }}{{ titlesuffix }} + {{ page_title|e }} | Vortex {%- endblock %} {%- if meta and meta.description %} {%- endif %} - + {%- if pageurl %} {%- endif %} + {#- The shared theme (ThemeSwitch): the reader's choice from the vortex-theme cookie, set before the + stylesheets so the wrong theme never flashes. With no script, data-theme="dark" gives the + default theme. #} + {%- for css in css_files %} {{ css_tag(css) }} {%- endfor %} + {%- block scripts %} {%- for js in script_files %} {{ js_tag(js) }} @@ -40,39 +50,44 @@ {%- if pageurl %} {%- endif %} - {%- if hasdoc('genindex') %} + {%- if hasdoc('genindex') and not is_404 %} {%- endif %} {%- if hasdoc('search') %} - + {%- endif %} {%- block extrahead %}{% endblock %} {{ _('Skip to content') }} - -
- - - - - Vortex - - {{ _('Docs') }} - - - - - - + +{#- The shared header (the design system's Header): the wordmark, the surfaces' links with Docs + current, the search and the sidebar's button in the docs' tools, then the theme switch, GitHub, + and the Menu. #} +{%- macro nav_links() %} +{{ _('Docs') }}{{ _('Bench') }}{{ _('Explorer') }}{{ _('Blog') }}Slack +{%- endmacro %} +{%- macro theme_switch() %} +
+{%- endmacro %} +
+
VortexVortex
+ +
+ + + +
+
+ {{ theme_switch() }} + vortex-data/vortex +
+ {{ _('Menu') }} + +
+
+{%- block frame %}
-
-

Copyright © Vortex a Series of LF Projects, LLC. For web site terms of use, trademark policy and other project policies please see lfprojects.org/policies.

-
-
- {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - {header.column.getIsSorted() === 'asc' && ( - ▲ +
showTip(col) : undefined} + onPointerLeave={stats ? () => showTip(null) : undefined} + > + {canSort ? ( + + ) : ( + title )} - {header.column.getIsSorted() === 'desc' && ( - ▼ + {stats && tipColumn === col && ( + )} - -
+ {items.length > 0 && ( +
{flexRender(cell.column.columnDef.cell, cell.getContext())}