Refactor the TypeScript data flow for full type safety and auto-generation of Rust types (#3865)

* Migrate Specta to Tsify to auto-generate messages.ts, working except colors and widgets

* Adopt the generated FillColor/Color/GradientStops

* Fix widget typing

* Separate WidgetGroup enum variants into wrapper structs

* Small rename

* Simplify widgets further

* Clean up message type references

* Switch type imports to the auto-generated file

* Remove lowercase serde rename

* Fix FillChoice deserialization

* Fix small regression from #3837

* Improve type safety

* Make WidgetSpan type-safe

* More cleanup and type safety

* More type safety

* More type safety

* Get the rest to type-check without errors; improve widget builder macro to have optional icons; improve Svelte 5 configs

* Cargo fmt

* Fix imports

* Update outdated readme info

* Fix lint command rename references

* Fix typos

* One more typos fix

* Remove unnecessary dep: prefix from the edited Cargo.toml files

* Remove excess parts from Cargo.toml

* Fix compiling on desktop

* Revert "Remove excess parts from Cargo.toml"

This reverts commit 6b711117b3a5d5d8a3ee20f36a43bc74930b7c82.

* Update dev docs with simpler, more accurate instructions
This commit is contained in:
Keavon Chambers
2026-03-09 16:35:04 -07:00
parent fbd2658148
commit 52d2b38a82
199 changed files with 2265 additions and 2811 deletions
@@ -228,42 +228,38 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
})
};
widgets.push(LayoutGroup::Row {
widgets: vec![TextLabel::new("Grid").bold(true).widget_instance()],
});
widgets.push(LayoutGroup::row(vec![TextLabel::new("Grid").bold(true).widget_instance()]));
widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Type").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
RadioInput::new(vec![
RadioEntryData::new("rectangular").label("Rectangular").on_update(update_val(grid, |grid, _| {
if let GridType::Isometric { y_axis_spacing, angle_a, angle_b } = grid.grid_type {
grid.isometric_y_spacing = y_axis_spacing;
grid.isometric_angle_a = angle_a;
grid.isometric_angle_b = angle_b;
}
grid.grid_type = GridType::Rectangular { spacing: grid.rectangular_spacing };
})),
RadioEntryData::new("isometric").label("Isometric").on_update(update_val(grid, |grid, _| {
if let GridType::Rectangular { spacing } = grid.grid_type {
grid.rectangular_spacing = spacing;
}
grid.grid_type = GridType::Isometric {
y_axis_spacing: grid.isometric_y_spacing,
angle_a: grid.isometric_angle_a,
angle_b: grid.isometric_angle_b,
};
})),
])
.min_width(200)
.selected_index(Some(match grid.grid_type {
GridType::Rectangular { .. } => 0,
GridType::Isometric { .. } => 1,
}))
.widget_instance(),
],
});
widgets.push(LayoutGroup::row(vec![
TextLabel::new("Type").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
RadioInput::new(vec![
RadioEntryData::new("rectangular").label("Rectangular").on_update(update_val(grid, |grid, _| {
if let GridType::Isometric { y_axis_spacing, angle_a, angle_b } = grid.grid_type {
grid.isometric_y_spacing = y_axis_spacing;
grid.isometric_angle_a = angle_a;
grid.isometric_angle_b = angle_b;
}
grid.grid_type = GridType::Rectangular { spacing: grid.rectangular_spacing };
})),
RadioEntryData::new("isometric").label("Isometric").on_update(update_val(grid, |grid, _| {
if let GridType::Rectangular { spacing } = grid.grid_type {
grid.rectangular_spacing = spacing;
}
grid.grid_type = GridType::Isometric {
y_axis_spacing: grid.isometric_y_spacing,
angle_a: grid.isometric_angle_a,
angle_b: grid.isometric_angle_b,
};
})),
])
.min_width(200)
.selected_index(Some(match grid.grid_type {
GridType::Rectangular { .. } => 0,
GridType::Isometric { .. } => 1,
}))
.widget_instance(),
]));
let mut color_widgets = vec![
TextLabel::new("Display").table_align(true).widget_instance(),
@@ -288,80 +284,72 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
}))
.widget_instance(),
);
widgets.push(LayoutGroup::Row { widgets: color_widgets });
widgets.push(LayoutGroup::row(color_widgets));
widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Origin").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(grid.origin.x))
.label("X")
.unit(" px")
.min_width(98)
.on_update(update_origin(grid, |grid| Some(&mut grid.origin.x)))
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
NumberInput::new(Some(grid.origin.y))
.label("Y")
.unit(" px")
.min_width(98)
.on_update(update_origin(grid, |grid| Some(&mut grid.origin.y)))
.widget_instance(),
],
});
widgets.push(LayoutGroup::row(vec![
TextLabel::new("Origin").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(grid.origin.x))
.label("X")
.unit(" px")
.min_width(98)
.on_update(update_origin(grid, |grid| Some(&mut grid.origin.x)))
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
NumberInput::new(Some(grid.origin.y))
.label("Y")
.unit(" px")
.min_width(98)
.on_update(update_origin(grid, |grid| Some(&mut grid.origin.y)))
.widget_instance(),
]));
match grid.grid_type {
GridType::Rectangular { spacing } => widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Spacing").table_align(true).widget_instance(),
GridType::Rectangular { spacing } => widgets.push(LayoutGroup::row(vec![
TextLabel::new("Spacing").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(spacing.x))
.label("X")
.unit(" px")
.min(0.)
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.x)))
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
NumberInput::new(Some(spacing.y))
.label("Y")
.unit(" px")
.min(0.)
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y)))
.widget_instance(),
])),
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => {
widgets.push(LayoutGroup::row(vec![
TextLabel::new("Y Spacing").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(spacing.x))
.label("X")
NumberInput::new(Some(y_axis_spacing))
.unit(" px")
.min(0.)
.min_width(200)
.on_update(update_origin(grid, |grid| grid.grid_type.isometric_y_spacing()))
.widget_instance(),
]));
widgets.push(LayoutGroup::row(vec![
TextLabel::new("Angles").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(angle_a))
.unit("°")
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.x)))
.on_update(update_origin(grid, |grid| grid.grid_type.angle_a()))
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
NumberInput::new(Some(spacing.y))
.label("Y")
.unit(" px")
.min(0.)
NumberInput::new(Some(angle_b))
.unit("°")
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y)))
.on_update(update_origin(grid, |grid| grid.grid_type.angle_b()))
.widget_instance(),
],
}),
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => {
widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Y Spacing").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(y_axis_spacing))
.unit(" px")
.min(0.)
.min_width(200)
.on_update(update_origin(grid, |grid| grid.grid_type.isometric_y_spacing()))
.widget_instance(),
],
});
widgets.push(LayoutGroup::Row {
widgets: vec![
TextLabel::new("Angles").table_align(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(angle_a))
.unit("°")
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.angle_a()))
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
NumberInput::new(Some(angle_b))
.unit("°")
.min_width(98)
.on_update(update_origin(grid, |grid| grid.grid_type.angle_b()))
.widget_instance(),
],
});
]));
}
}
@@ -42,7 +42,8 @@ pub enum GizmoEmphasis {
// TODO Remove duplicated definition of this in `utility_types_web.rs`
/// Types of overlays used by DocumentMessage to enable/disable the selected set of viewport overlays.
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum OverlaysType {
ArtboardName,
CompassRose,
@@ -60,7 +61,8 @@ pub enum OverlaysType {
}
// TODO Remove duplicated definition of this in `utility_types_web.rs`
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct OverlaysVisibilitySettings {
pub all: bool,
@@ -160,11 +162,11 @@ impl OverlaysVisibilitySettings {
}
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct OverlayContext {
// Serde functionality isn't used but is required by the message system macros
#[serde(skip)]
#[specta(skip)]
internal: Arc<Mutex<OverlayContextInternal>>,
pub viewport: ViewportMessageHandler,
pub visibility_settings: OverlaysVisibilitySettings,
@@ -35,7 +35,8 @@ pub enum GizmoEmphasis {
}
/// Types of overlays used by DocumentMessage to enable/disable the selected set of viewport overlays.
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum OverlaysType {
ArtboardName,
CompassRose,
@@ -52,7 +53,8 @@ pub enum OverlaysType {
Handles,
}
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct OverlaysVisibilitySettings {
pub all: bool,
@@ -150,11 +152,11 @@ impl OverlaysVisibilitySettings {
}
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct OverlayContext {
// Serde functionality isn't used but is required by the message system macros
#[serde(skip, default = "overlay_canvas_context")]
#[specta(skip)]
pub render_context: web_sys::CanvasRenderingContext2d,
pub viewport: ViewportMessageHandler,
pub visibility_settings: OverlaysVisibilitySettings,