diff --git a/light9/live/Effect.ts b/light9/live/Effect.ts --- a/light9/live/Effect.ts +++ b/light9/live/Effect.ts @@ -4,6 +4,7 @@ import { some } from "underscore"; import { Patch, patchContainsPreds, patchUpdate } from "../web/patch"; import { SyncedGraph } from "../web/SyncedGraph"; import { shortShow } from "../web/show_specific"; +import { SubEvent } from "sub-events"; // todo: Align these names with newtypes.py, which uses HexColor and VTUnion. type Color = string; @@ -15,11 +16,13 @@ function isUri(x: Term | number | string return typeof x == "object" && x.termType == "NamedNode"; } +// todo: eliminate this. address the scaling when we actually scale +// stuff, instead of making a mess of every setting function valuePred(graph: SyncedGraph, attr: NamedNode): NamedNode { const U = graph.U(); const scaledAttributeTypes = [U(":color"), U(":brightness"), U(":uv")]; if (some(scaledAttributeTypes, (x: NamedNode) => attr.equals(x))) { - return U(":scaledValue"); + return U(":value"); } else { return U(":value"); } @@ -29,11 +32,12 @@ function valuePred(graph: SyncedGraph, a export class Effect { private settings: Array<{ device: NamedNode; deviceAttr: NamedNode; setting: NamedNode; value: ControlValue }> = []; private ctxForEffect: NamedNode + settingsChanged:SubEvent=new SubEvent() constructor( public graph: SyncedGraph, public uri: NamedNode, // called if the graph changes our values and not when the caller uses edit() - private onValuesChanged: (values: void) => void + // private onValuesChanged: (values: void) => void ) { this.ctxForEffect = this.graph.Uri(this.uri.value.replace("light9.bigasterisk.com/effect", `light9.bigasterisk.com/show/${shortShow}/effect`)); graph.runHandler(this.rebuildSettingsFromGraph.bind(this), `effect sync ${uri.value}`); @@ -61,7 +65,7 @@ export class Effect { // return; } - // log("syncFromGraph", this.uri); + log("syncFromGraph", this.uri); // this repeats work- it gathers all settings when really some values changed (and we might even know about them). maybe push the value-fetching into a secnod phase of the run, and have the 1st phase drop out early const newSettings = []; @@ -69,7 +73,7 @@ export class Effect { const seenDevAttrPairs: Set = new Set(); for (let setting of Array.from(this.graph.objects(this.uri, U(":setting")))) { - // log(` setting ${setting.value}`); + log(` setting ${setting.value}`); if (!isUri(setting)) throw new Error(); let value: ControlValue; const device = this.graph.uriValue(setting, U(":device")); @@ -92,9 +96,11 @@ export class Effect { newSettings.push({ device, deviceAttr, setting, value }); } + log(newSettings) this.settings = newSettings; log(`rebuild to ${this.settings.length}`); - this.onValuesChanged(); + this.settingsChanged.emit() + // this.onValuesChanged(); } currentValue(device: NamedNode, deviceAttr: NamedNode): ControlValue | null { diff --git a/light9/live/Light9LiveControl.ts b/light9/live/Light9AttrControl.ts rename from light9/live/Light9LiveControl.ts rename to light9/live/Light9AttrControl.ts --- a/light9/live/Light9LiveControl.ts +++ b/light9/live/Light9AttrControl.ts @@ -1,24 +1,25 @@ import debug from "debug"; import { css, html, LitElement, PropertyValues } from "lit"; -import { customElement, property } from "lit/decorators.js"; +import { customElement, property, state } from "lit/decorators.js"; import { Literal, NamedNode } from "n3"; import { SubEvent } from "sub-events"; import { SyncedGraph } from "../web/SyncedGraph"; -import { ControlValue } from "./Effect"; +import { ControlValue, Effect } from "./Effect"; import { GraphToControls } from "./GraphToControls"; import { DeviceAttrRow } from "./Light9DeviceControl"; import { Choice } from "./Light9Listbox"; +import { getTopGraph } from "../web/RdfdbSyncedGraph"; export { Slider } from "@material/mwc-slider"; export { Light9ColorPicker } from "../web/light9-color-picker"; -const log = debug("control"); +const log = debug("settings.dev.attr"); - -const makeType = (d: "scalar" | "color" | "choice") => new NamedNode(`http://light9.bigasterisk.com/${d}`); +type DataTypeNames = "scalar" | "color" | "choice"; +const makeType = (d: DataTypeNames) => new NamedNode(`http://light9.bigasterisk.com/${d}`); // UI for one device attr (of any type). -@customElement("light9-live-control") -export class Light9LiveControl extends LitElement { +@customElement("light9-attr-control") +export class Light9AttrControl extends LitElement { graph!: SyncedGraph; static styles = [ @@ -30,67 +31,132 @@ export class Light9LiveControl extends L #colorControls > * { margin: 0 3px; } - :host { - border: 2px solid white; - } + :host { + border: 2px solid white; + } + mwc-slider { + width: 250px; + } `, ]; - // passed from parent - @property() device!: NamedNode; - @property() dataType: NamedNode; - @property() deviceAttrRow!: DeviceAttrRow; + @property() deviceAttrRow: DeviceAttrRow | null = null; + @state() dataType: DataTypeNames = "scalar"; + + @property() effect: Effect | null = null; // we'll connect to this and receive graphValueChanged and send uiValueChanged - @property() graphToControls!: GraphToControls; + // @property() graphToControls!: GraphToControls; @property() enableChange: boolean = false; - @property() value: ControlValue | null = null; + @property() value: ControlValue | null = null; // e.g. color string // slider mode - @property() sliderValue: number = 0; + // @property() sliderValue: number = 0; // color mode // choice mode - @property() pickedChoice: Choice | null = null; - @property() choiceValue: Choice | null = null; + // @property() pickedChoice: Choice | null = null; + // @property() choiceValue: Choice | null = null; valueChanged: SubEvent = new SubEvent(); constructor() { super(); - this.dataType = makeType("color"); - // getTopGraph().then((g) => { - // this.graph = g; - // // this.graph.runHandler(this.graphReads.bind(this), `${this.device} ${this.deviceAttrRow.uri} reads`); - // }); + getTopGraph().then((g) => { + this.graph = g; + if (this.deviceAttrRow === null) throw new Error(); + // this.graph.runHandler(this.graphReads.bind(this), `${this.deviceAttrRow.device} ${this.deviceAttrRow.uri} reads`); + }); + } + + connectedCallback(): void { + super.connectedCallback(); } render() { - const dbg=html` - ]` - if (this.dataType.equals(makeType("scalar"))) { - return html`${dbg} `; - } else if (this.dataType.equals(makeType("color"))) { - return html` ${dbg} + if (this.deviceAttrRow === null) throw new Error(); + const dbg = html` live-control ${this.dataType} ]`; + if (this.dataType == "scalar") { + const v = this.value || 0; + return html`${dbg} `; + } else if ((this.dataType = "color")) { + const v = this.value || '#000' + return html` + ${dbg}
- +
`; - } else if (this.dataType.equals(makeType("choice"))) { - return html`${dbg} `; + } else if (this.dataType == "choice") { + return html`${dbg} `; } } // graphReads() { + // if (this.deviceAttrRow === null) throw new Error(); // const U = this.graph.U(); + // this.effect?.currentValue(this.deviceAttrRow.device, this.deviceAttrRow.uri); // } - updated(changedProperties: PropertyValues) { - if (changedProperties.has("graphToControls")) { - this.graphToControls.register(this.device, this.deviceAttrRow.uri, this.onGraphValueChanged.bind(this)); - this.enableChange = true; + updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + // if (changedProperties.has("graphToControls")) { + // // this.graphToControls.register(this.device, this.deviceAttrRow.uri, this.onGraphValueChanged.bind(this)); + // this.enableChange = true; + // } + + if (changedProperties.has("deviceAttrRow")) { + this.onDeviceAttrRowProperty(); + } + if (changedProperties.has("effect")) { + this.onEffectProperty(); + } + if (changedProperties.has("value")) { + this.onValueProperty(); + } + } + + private onValueProperty() { + if (this.deviceAttrRow === null) throw new Error(); + if (this.effect !== null && this.graph !== undefined) { + const p = this.effect.edit(this.deviceAttrRow.device, this.deviceAttrRow.uri, this.value); + log("patch", p, "to", this.graph); + if (p.adds.length || p.dels.length) { + this.graph.applyAndSendPatch(p); + } + } + } + + private onEffectProperty() { + if (this.effect === null) throw new Error(); + // effect will read graph changes on its own, but emit an event when it does + this.effect.settingsChanged.subscribe(() => { + this.effectSettingsChanged(); + }); + this.effectSettingsChanged(); + } + + private effectSettingsChanged() { + // anything in the settings graph is new + log("i check the effect current value"); + if (this.deviceAttrRow === null) throw new Error(); + if (this.effect === null) throw new Error(); + log("graph->ui on ", this.deviceAttrRow.device, this.deviceAttrRow.uri); + const v=this.effect.currentValue(this.deviceAttrRow.device, this.deviceAttrRow.uri); + this.onGraphValueChanged(v); + } + + private onDeviceAttrRowProperty() { + if (this.deviceAttrRow === null) throw new Error(); + const d = this.deviceAttrRow.dataType; + if (d.equals(makeType("scalar"))) { + this.dataType = "scalar"; + } else if (d.equals(makeType("color"))) { + this.dataType = "color"; + } else if (d.equals(makeType("choice"))) { + this.dataType = "choice"; } } @@ -99,23 +165,32 @@ export class Light9LiveControl extends L // not sure what this is, but it seems to be followed by good events return; } - log(ev.type, ev.detail?.value); - this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, ev.detail.value); + log(ev.type, ev.detail.value); + this.value = ev.detail.value; + // this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, ev.detail.value); } +onColorInput(ev: CustomEvent) { + this.value = ev.detail.value; +} + onGraphValueChanged(v: ControlValue | null) { - // log("change: control must display", v); + if (this.deviceAttrRow === null) throw new Error(); + log("change: control must display", v, "for", this.deviceAttrRow.device.value, this.deviceAttrRow.uri.value); // this.enableChange = false; - if (this.dataType.equals(makeType("scalar"))) { + if (this.dataType == "scalar") { if (v !== null) { setTimeout(() => { // only needed once per page layout this.shadowRoot?.querySelector("mwc-slider")?.layout(/*skipUpdateUI=*/ false); }, 1); - this.sliderValue = v as number; + this.value = v; } else { - this.sliderValue = 0; + this.value = 0; } + } else if (this.dataType == "color") { + log('graph sets coolor', v) + this.value=v; } // if (v === null) { // this.clear(); @@ -128,34 +203,47 @@ export class Light9LiveControl extends L // this.enableChange = true; } + graphToColor(v: ControlValue | null) { + this.value = v === null ? "#000" : v; + return; + // const cp = this.shadowRoot?.querySelector("light9-color-picker") as Light9ColorPicker | null; + // if (cp) { + // if (typeof v != "string") throw new Error("type v is " + typeof v); + // if (v === null) { + // v = "#000"; + // } + // cp.setColor(v as string); + // } + } + goBlack() { this.value = "#000000"; } onChoice(value: any) { - if (this.graphToControls == null || !this.enableChange) { - return; - } - if (value != null) { - value = this.graph.Uri(value); - } else { - value = null; - } - this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); + // if (this.graphToControls == null || !this.enableChange) { + // return; + // } + // if (value != null) { + // value = this.graph.Uri(value); + // } else { + // value = null; + // } + // this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); } onChange(value: any) { - if (this.graphToControls == null || !this.enableChange) { - return; - } - if (typeof value === "number" && isNaN(value)) { - return; - } // let onChoice do it - //log('change: control tells graph', @deviceAttrRow.uri.value, value) - if (value === undefined) { - value = null; - } - this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); + // if (this.graphToControls == null || !this.enableChange) { + // return; + // } + // if (typeof value === "number" && isNaN(value)) { + // return; + // } // let onChoice do it + // //log('change: control tells graph', @deviceAttrRow.uri.value, value) + // if (value === undefined) { + // value = null; + // } + // this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); } // clear() { diff --git a/light9/live/Light9DeviceControl.ts b/light9/live/Light9DeviceControl.ts --- a/light9/live/Light9DeviceControl.ts +++ b/light9/live/Light9DeviceControl.ts @@ -8,13 +8,15 @@ import { getTopGraph } from "../web/Rdfd import { SyncedGraph } from "../web/SyncedGraph"; import { GraphToControls } from "./GraphToControls"; import { Choice } from "./Light9Listbox"; -import { Light9LiveControl } from "./Light9LiveControl"; +import { Light9AttrControl } from "./Light9AttrControl"; +import { Effect } from "./Effect"; export { ResourceDisplay } from "../web/ResourceDisplay"; -export { Light9LiveControl }; -const log = debug("devcontrol"); +export { Light9AttrControl }; +const log = debug("settings.dev"); export interface DeviceAttrRow { uri: NamedNode; //devattr + device: NamedNode; attrClasses: string; // the css kind dataType: NamedNode; showColorPicker: boolean; @@ -26,6 +28,7 @@ export interface DeviceAttrRow { max: number; } +// Widgets for one device with multiple Light9LiveControl rows for the attr(s). @customElement("light9-device-control") export class Light9DeviceControl extends LitElement { graph!: SyncedGraph; @@ -88,13 +91,13 @@ export class Light9DeviceControl extends ${this.deviceAttrs.map( (dattr: DeviceAttrRow) => html`
- attr - + + attr + + + + .graphToControls={this.graphToControls} +
` )} @@ -103,8 +106,8 @@ export class Light9DeviceControl extends } @property() uri!: NamedNode; - @property() effect!: NamedNode; - @property() graphToControls!: GraphToControls; + @property() effect!: Effect; + // @property() graphToControls!: GraphToControls; @property() devClasses: string = ""; // the css kind @property() deviceAttrs: DeviceAttrRow[] = []; @@ -151,9 +154,9 @@ export class Light9DeviceControl extends return; } try { - this.deviceClass = this.graph.uriValue(this.uri, U("rdf:type")); - } catch(e) { - // what's likely is we're going through a graph reload and the graph + this.deviceClass = this.graph.uriValue(this.uri, U("rdf:type")); + } catch (e) { + // what's likely is we're going through a graph reload and the graph // is gone but the controls remain } this.deviceAttrs = []; @@ -169,6 +172,7 @@ export class Light9DeviceControl extends const dataType = this.graph.uriValue(devAttr, U(":dataType")); const daRow = { uri: devAttr, + device: this.uri, dataType, showColorPicker: dataType.equals(U(":color")), attrClasses: this.selectedAttrs.has(devAttr) ? "selected" : "", @@ -206,8 +210,8 @@ export class Light9DeviceControl extends clear() { // why can't we just set their values ? what's diff about // the clear state, and should it be represented with `null` value? - throw new Error() - Array.from(this.shadowRoot!.querySelectorAll("light9-live-control")).map((lc: Element) => (lc as Light9LiveControl).clear()); + throw new Error(); + // Array.from(this.shadowRoot!.querySelectorAll("light9-live-control")).map((lc: Element) => (lc as Light9LiveControl).clear()); } onClick(ev: any) { diff --git a/light9/live/Light9LiveControls.ts b/light9/live/Light9DeviceSettings.ts rename from light9/live/Light9LiveControls.ts rename to light9/live/Light9DeviceSettings.ts --- a/light9/live/Light9LiveControls.ts +++ b/light9/live/Light9DeviceSettings.ts @@ -7,12 +7,13 @@ import { Patch, patchContainsPreds } fro import { getTopGraph } from "../web/RdfdbSyncedGraph"; import { SyncedGraph } from "../web/SyncedGraph"; import { GraphToControls } from "./GraphToControls"; +import { Effect } from "./Effect"; export { EditChoice } from "../web/EditChoice"; export { Light9DeviceControl as Light9LiveDeviceControl } from "./Light9DeviceControl"; -const log = debug("controls"); +const log = debug("settings"); -@customElement("light9-live-controls") -export class Light9LiveControls extends LitElement { +@customElement("light9-device-settings") +export class Light9DeviceSettings extends LitElement { graph!: SyncedGraph; static styles = [ @@ -43,12 +44,12 @@ export class Light9LiveControls extends return html` -

effect deviceattrs

+

effect EeviceSettings

- +
@@ -56,7 +57,7 @@ export class Light9LiveControls extends
${this.devices.map( (device: NamedNode) => html` - + .graphToControls={this.graphToControls} ` )}
@@ -66,8 +67,8 @@ export class Light9LiveControls extends devices: Array = []; // uri of the effect being edited, or null. This is the // master value; GraphToControls follows. - @property() effectChoice: NamedNode | null = null; - graphToControls!: GraphToControls; + @property() currentEffect: Effect | null = null; + // graphToControls!: GraphToControls; okToWriteUrl: boolean = false; constructor() { @@ -76,19 +77,26 @@ export class Light9LiveControls extends getTopGraph().then((g) => { this.graph = g; this.graph.runHandler(this.findDevices.bind(this), "findDevices"); - this.graphToControls = new GraphToControls(this.graph); + // this.graphToControls = new GraphToControls(this.graph); // this.graph.runHandler(this.update.bind(this), "Light9LiveControls update"); this.setEffectFromUrl(); }); } onEffectChoice2(ev: CustomEvent) { - this.effectChoice = ev.detail.newValue as NamedNode; + const uri = ev.detail.newValue as NamedNode; + if (uri === null) { + this.currentEffect = null; + } else { + this.currentEffect = new Effect(this.graph, uri); + } } - updated(changedProperties: PropertyValues) { - if (changedProperties.has("effectChoice")) { - log(`effectChoice to ${this.effectChoice?.value}`); + updated(changedProperties: PropertyValues) { + log("ctls udpated", changedProperties); + if (changedProperties.has("currentEffect")) { + log(`effectChoice to ${this.currentEffect?.uri?.value}`); this.onEffectChoice(); } + // this.graphToControls?.debugDump(); } // Note that this doesn't fetch setting values, so it only should get rerun @@ -117,12 +125,12 @@ export class Light9LiveControls extends const effect = new URL(window.location.href).searchParams.get("effect"); if (effect != null) { log(`found effect in url ${effect}`); - this.effectChoice = this.graph.Uri(effect); + this.currentEffect = new Effect(this.graph, this.graph.Uri(effect)); } this.okToWriteUrl = true; } - writeToUrl(effect: NamedNode | null) { + writeToUrl(effect: NamedNode | undefined) { const effectStr = effect ? this.graph.shorten(effect) : ""; if (!this.okToWriteUrl) { return; @@ -137,30 +145,30 @@ export class Light9LiveControls extends } newEffect() { - this.effectChoice = this.graphToControls.newEffect(); + // this.effectChoice = this.graphToControls.newEffect(); } onEffectChoice() { const U = (x: any) => this.graph.Uri(x); - if (this.effectChoice == null) { - // unlink - log("onEffectChoice unlink"); - if (this.graphToControls != null) { - this.graphToControls.setEffect(null); - } - } else { - if (this.graphToControls != null) { - this.graphToControls.setEffect(this.effectChoice); - } else { - throw new Error("graphToControls not set"); - } - } - this.writeToUrl(this.effectChoice); + // if (this.effectChoice == null) { + // // unlink + // log("onEffectChoice unlink"); + // if (this.graphToControls != null) { + // this.graphToControls.setEffect(null); + // } + // } else { + // if (this.graphToControls != null) { + // this.graphToControls.setEffect(this.effectChoice); + // } else { + // throw new Error("graphToControls not set"); + // } + // } + this.writeToUrl(this.currentEffect?.uri); } clearAll() { // clears the effect! - return this.graphToControls.emptyEffect(); + return; //this.graphToControls.emptyEffect(); } // configureFromGraph() { diff --git a/light9/live/index.html b/light9/live/index.html --- a/light9/live/index.html +++ b/light9/live/index.html @@ -1,10 +1,10 @@ - device control + device settings - + - + diff --git a/light9/web/light9-color-picker.ts b/light9/web/light9-color-picker.ts --- a/light9/web/light9-color-picker.ts +++ b/light9/web/light9-color-picker.ts @@ -58,6 +58,7 @@ export class Light9ColorPicker extends L pickerFloat.pageInit(); } update(changedProperties: PropertyValueMap) { + super.update(changedProperties); if (changedProperties.has("color")) { this.setColor(this.color); } @@ -66,11 +67,12 @@ export class Light9ColorPicker extends L .value(this.value / 255) .hex(); + this.dispatchEvent(new CustomEvent("input", { detail: { value: this.color } })); + this.swatchEl.then((sw) => { sw.style.borderColor = this.hueSatColor; }); } - super.update(changedProperties); } private onVSliderChange(ev: CustomEvent) {